1 Macro
racket 的 macro 是給了一個編譯時期運行 racket 的環境,又因為 racket 是 s expression,所以可以直接操作 racket ast(car、cdr 等),因此也稱 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)))
這裏可以看到為什麼要選 s expression(或是任意一種資料即程式的表達方式了),建構 ast(當然是一種資料)跟寫原始程式沒有任何差別。
(let ([a 1] [b 2]) (swap a b) (displayln (list a b)))
然而如果我們把 a、b 改成 x1、b 呢?直覺上我們會因為變數覆蓋而得到錯誤的結果:
(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 並不是單純的照搬程式碼進來而已,它會保證 x、y 不會跟內部定義的變數衝突(當然可以想見這實現起來有多麻煩),這種不污染 macro 內的概念就叫做 hygienic macro。
但用 define-syntax-rule 我們只能有一種形式,要怎麼做出像是 define 這樣有多種變化的 form 呢?這時候我們就需要 define-syntax 跟 syntax-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))
那 x、y 就會綁定到 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 明確地把回傳值包裝起來,這樣我們才能區分檢查的程式跟組合出來的結果。