Racket 進階教學
1 Macro
1.1 Pattern-Based Macros
2 TODO
8.1

Racket 進階教學

Lîm Tsú-thuàn <racket@racket.tw>

1 Macro

racket 的 macro 是給了一個編譯時期運行 racket 的環境,又因為 racket 是 s expression,所以可以直接操作 racket ast(carcdr 等),因此也稱 syntax transformers。那麼趕緊來看怎麼使用吧!

1.1 Pattern-Based Macros

定義 macro 最簡單的方式就是使用 define-syntax-rule

(define-syntax-rule pattern template)

假設我們定義了一個 swap macro,它會交換兩個變數中儲存的值,那麼我們可以用 define-syntax-rule 定義它:

(define-syntax-rule (swap x y)
  (let ([x1 x])
    (set! x y)
    (set! y x1)))

這個定義可以解釋為:
  • 接受 xx 兩個變數

  • 回傳
    (let ([x1 x])
      (set! x y)
      (set! y x1))
    這個 ast

這裏可以看到為什麼要選 s expression(或是任意一種資料即程式的表達方式了),建構 ast(當然是一種資料)跟寫原始程式沒有任何差別。

(let ([a 1]
      [b 2])
  (swap a b)
  (displayln (list a b)))

然而如果我們把 ab 改成 x1b 呢?直覺上我們會因為變數覆蓋而得到錯誤的結果:

(let ([x1 1]
      [b 2])
  (let ([x1 x1])
    (set! x1 y)
    (set! y x1))
  (displayln (list x1 b)))

但 Racket 卻產生了正確的結果:

(define-syntax-rule (swap x y)
  (let ([x1 x])
    (set! x y)
    (set! y x1)))

 

> (let ([tmp 1]
        [d 2])
    (swap tmp d)
    (displayln (list tmp d)))

(2 1)

這是因為 Racket 並不是單純的照搬程式碼進來而已,它會保證 xy 不會跟內部定義的變數衝突(當然可以想見這實現起來有多麻煩),這種不污染 macro 內的概念就叫做 hygienic macro

但用 define-syntax-rule 我們只能有一種形式,要怎麼做出像是 define 這樣有多種變化的 form 呢?這時候我們就需要 define-syntaxsyntax-rules 了!

(define-syntax id
  (syntax-rules (literal-id ...)
    [pattern template]
    ...))

現在我們建立 my-define,單純包裝 define

(define-syntax my-define
  (syntax-rules ()
    [(my-define x e)
     (define x e)]
    [(my-define (x p ...) e)
     (define (x p ...) e)]))

雖然這很無聊但是這個例子只是要說明我們怎麼讓一個 form 對到不同模式,同時這裏也用到了 ...,這個模式用來一次比對多個,例如我們寫:

(my-define (add x y)
  (+ x y))

xy 就會綁定到 p 變成 '(x y),接著底下 macro body 的部分又會把 p ... 展開。

然而現在的做法還有一些問題,我們可以寫:

(my-define 1 1)
(swap 'a 'b)

而這些程式是荒謬的,雖然這兩個例子裡面轉換後的 form 剛好能抓到問題,然而一來這未必會成立,二來錯誤訊息跟我們定義的 form 毫無關聯,對使用者而言非常難讀。而使用 syntax-case 可以解決這個問題:

(define-syntax my-define
  (λ (stx)
    (syntax-case stx ()
      [(my-define x e)
       (unless (identifier? #'x)
         (error 'my-define "~a should be an identifier" #'x))
       #'(define x e)]
      [(my-define (x p ...) e)
       (unless (identifier? #'x)
         (error 'my-define "~a should be an identifier" #'x))
       #'(define (x p ...) e)])))

syntax-case 在這裡必須被包在 λ 底下,好接收 stx 參數。並且這裏需要用 syntax 明確地把回傳值包裝起來,這樣我們才能區分檢查的程式跟組合出來的結果。

2 TODO