使用 explicit renaming 實現衛生巨集
2018-11-04
接上篇,前面對 scheme 衛生巨集的實現方式有個整體的介紹,這一次具體講其中 explicit renaming 這種方式的實現原理。
首先講Lambda_calculus#%CE%B1-conversion" rel="nofollow,noindex" target="_blank">alpha 變換 。alpha 變換這東西,說的就是函式的引數名字其實是無所謂的。
(lambda (a b) (+ a b))
跟下面這個完全是等價的。
(lambda (a1 b2) (+ a1 b2))
alpha 變換就是自由的把函式名字隨便換。alpha 變換這麼實現:
(define (alpha-convert exp env) (match (x (symbol? exp)) (replace-symbol x env)) (lambda (x) body) (let ((new-env (cons (x . x1) env))) `(lambda (x) ,(alpha-convert body new-env))))
用一個環境,環境裡面是(原符號 . rename後的符號)
。
為什麼要講 alpha 變換呢? 因為 explicit renaming 其核心就是在做 alpha 變換!
考慮一個巨集:
(swap! x y) (let ((tmp x)) (set! x y) (set! y tmp))
如果我們把展開巨集顯式的重新命名一下,注意前面說過的衛生的原則 -- 衛生的本質問題,還是作用域。把需要使用 巨集定義時作用域 的變數用 [] 框起來:
([let] (([tmp] x)) ([set!] x y) ([set!] y [tmp]))
[let] [set!] [tmp] 指代的是巨集定義時的符號,而 x y 則是巨集展開時期的符號。每一次巨集展開生成都賦予一個唯一 id,因此我們可以把上面寫成:
([let 1] (([tmp 1] x)) ([set! 1] x y) ([set! 1] y [tmp 1]))
如果有多次的巨集展開,比如 (swap! (or x 1) y) 第一次展開 swap! 時:
([let 1] (([tmp 1] (or x 1)) ([set! 1] (or x 1) y) ([set! 1] y [tmp 1]))
第二次再展開 or 之後:
([let 1] (([tmp 1] ([if 2] x x 1)) ([set! 1] ([if 2] x x 1) y) ([set! 1] y [tmp 1]))
注意其中的 [let 1] 跟 [if 2] 分別是對 swap! 和 or 的兩次展開過程,每次展開都對應了一個唯一 id。如果我們有一個 [let 1] 和 [let 3],它倆有可能是同一個東西,只是在不同的巨集展開過程中賦予的 id 不同。
如果看er-macro-transform
的說明,它說每次展開都是在不同的詞法環境中,所以不同的巨集展開後的符號不會跟其它任何地方衝突。怎麼理解的呢?嗯,其實就是做了 alpha 變換。對於前面的前面的例子,如果我們再夾雜一個 alpha 變換,可以寫成這樣子:
([let 1] (([tmp 1] x@523423132) ([set! 1] x@523423132 y@44512342) ([set! 1] y@44512342 [tmp 1]))
alpha 變換使用的 巨集展開時環境: ((x . x@523423132) (y . y@44512342))
這是最重點的地方,準確來說,遇到巨集展開會做兩步操作:
- 第一步是在巨集展開時,把 let set! 變成 [let 1] [set! 1]
- 第二步是將展開的結果做 alpha 替換,對普通的符號,使用巨集展開時環境替換;對於 [let 1] 這種,使用巨集定義時環境替換
或者放點程式碼會更清楚一點:
(define (expand exp menv env) (cond ((symbol? exp) (alpha-convert exp env)) ((generated? exp) (let ((env-of-def (assq (generated-uid exp) menv))) (alpha-convert (generated-sym exp) env))) ((pair? exp) (let ((den (binding (car exp) menv env))) (cond ((special? den) (expand-special-form den exp menv env)) ((macro? den) (expand-macro den exp menv env)) (else (expand-application exp menv env))))) (else exp)));; for const like string, number and so on
其中的 env 是一個符號,到一個繫結。繫結內容可能是 alpha 變換後的符號,或者特殊表 if begin set! lambda,也可能是巨集。
巨集的表示是 轉換函式,以及巨集定義時環境。generated 就是 (name . uid)
menv 是 uid 到 env 的對映,因為展開 generated 需要兩步,通過 generated-uid 從 menv 獲取到 generated 定義時的環境,下一步在是在該環境裡面對 generated-sym 做 alpha 變換。
(define (expand-macro mac exp menv env) (let* ((transform (macro-func mac)) ;;提取巨集轉換函式 (env-of-def (macro-env mac)) ;;提取巨集定義時環境 (uid (unique-id));;每次展開生成唯一 id (new-menv (cons (uid . env-of-def) menv)) ;;唯一 id 到 env 繫結 (rename (lambda (x) (make-generated x uid))) ;; rename 會傳遞給巨集用於繫結 巨集定義時環境 (new-exp (transform exp rename))) ;; 呼叫巨集轉換函式,會生成 [let uid] (expand new-exp new-menv env))) ;; 對巨集展開的結果,遞迴再展開
expand-special-form 是對 if begin set! lambda 等東西做展開,其中 lambda 的展開:
(define (expand-lambda exp menv env) (let* ((args (cadr exp));; (a b c) (values (map rename args));; (a@5 b@6 c@7) (binds (map cons args values)) ;; ((a . a@5) (b . b@6) (c . c@7)) (new-env (append binds env)));; 新的 env,用於 alpha 變換 `(lambda ,values ,@(expand-sequence (cddr exp) menv new-env))))
expand-special-forma 裡面另個比較特殊的是 expand-defmacro,它要將 mac 加到 env 裡面去。 應該解釋清楚了。
最後看一個問題是,為什麼 alpha 變換是衛生的,而 gensym 不是?
(let ((tmp#jsdjflsdf 32)) (some-macro))
在 some-macro 展開用(let ((tmp (gensym))))
引入的 tmp,可能正好是tmp#jsdjflsdf
,就會被前面的繫結誤捕獲了。而在 alpha 變換中,tmp#jsdjflsdf
在環境裡面是tmp#jsdjflsdf@123
,巨集展開時的 id 不可能是@123
,它只能是一個不一樣的 id 了。