16 宏(macro)
宏(macro)是一種語法表,它有一個關聯的轉換器(transformer),它將原有的表擴展(expand)爲現有的表。換句話說,宏是Racket編譯器的擴展。racket/base和racket的大部分句法表實際上是宏,擴展成一小部分核心結構。
像許多語言一樣,Racket提供基於模式的宏,使得簡單的轉換易於實現和可靠使用。Racket還支持任意的宏轉換器,它在Racket中實現,或在Racket中的宏擴展變體中實現。
(對於自下而上的Racket宏的介紹,你可以參考:《宏的擔憂》)
16.1 基於模式的宏
基於模式的宏(pattern-based macro)將任何與模式匹配的代碼替換爲使用與模式部分匹配的原始語法的一部分的擴展。
16.1.1 define-syntax-rule
創建宏的最簡單方法是使用define-syntax-rule:
(define-syntax-rule pattern template)
作爲一個運行的例子,考慮交換宏swap,它將交換值存儲在兩個變量中。可以使用define-syntax-rule實現如下:
(define-syntax-rule (swap x y) (let ([tmp x]) (set! x y) (set! y tmp)))
define-syntax-rule表綁定一個與單個模式匹配的宏。模式必須總是以一個開放的括號開頭,後面跟着一個標識符,這個標識符在這個例子中是swap。在初始標識符之後,其它標識符是宏模式變量(macro pattern variable),可以匹配宏使用中的任何內容。因此,這個宏匹配這個表(swap form1 form2)給任何form1和form2。
在define-syntax-rule中的模式之後是摸板(template)。模板用於替代與模式匹配的表,但模板中的模式變量的每個實例都替換爲宏使用模式變量匹配的部分。例如,在
(swap first last)
模式變量x匹配first及y匹配last,於是擴展是
(let ([tmp first]) (set! first last) (set! last tmp))
16.1.2 詞法範圍
假設我們使用swap宏來交換名爲tmp和other的變量:
(let ([tmp 5] [other 6]) (swap tmp other) (list tmp other))
上述表達式的結果應爲(6 5)。然而,這種swap的使用的單純擴展是
(let ([tmp 5] [other 6]) (swap tmp other) (list tmp other))
其結果是(5 6)。問題在於,這個單純的擴展混淆了上下文中的tmp,那裏swap與宏摸板中的tmp被使用。
Racket不會爲了swap的上述使用生成單純的擴展。相反,它會生成以下內容
(let ([tmp 5] [other 6]) (let ([tmp_1 tmp]) (set! tmp other) (set! other tmp_1)) (list tmp other))
正確的結果在(6 5)。同樣,在示例中
(let ([set! 5] [other 6]) (swap set! other) (list set! other))
其擴展是
(let ([set!_1 5] [other 6]) (let ([tmp_1 set!_1]) (set! set!_1 other) (set! other tmp_1)) (list set!_1 other))
因此局部set!綁定不會干擾宏模板引入的賦值。
換句話說,Racket的基於模式的宏自動維護詞法範圍,所以宏的實現者可以思考宏中的變量引用以及在同樣的途徑中作爲函數和函數調用的宏使用。
16.1.3 define-syntax和syntax-rules
define-syntax-rule表綁定一個與單一模式匹配的宏,但Racket的宏系統支持從同一標識符開始匹配多個模式的轉換器。要編寫這樣的宏,程序員必須使用更通用的define-syntax表以及syntax-rules轉換器表:
(define-syntax id (syntax-rules (literal-id ...) [pattern template] ...))
例如,假設我們希望一個rotate宏將swap概括爲兩個或三個標識符,因此
(let ([red 1] [green 2] [blue 3]) (rotate red green) ; swaps (rotate red green blue) ; rotates left (list red green blue))
生成(1 3 2)。我們可以使用syntax-rules實現 rotate:
(define-syntax rotate (syntax-rules () [(rotate a b) (swap a b)] [(rotate a b c) (begin (swap a b) (swap b c))]))
表達式(rotate red green)與syntax-rules表中的第一個模式相匹配,因此擴展到(swapred green)。表達式(rotate red green blue)與第二個模式匹配,所以它擴展到(begin(swap red green) (swap green blue))。
16.1.4 匹配序列
一個更好的rotate宏將允許任意數量的標識符,而不是隻有兩個或三個標識符。匹配任何數量的標識符的rotate使用,我們需要一個模式表,它有點像克林閉包(Kleene star)。在一個Racket宏模式中,一個閉包(star)被寫成...。
爲了用...實現rotate,我們需要一個基元(base case)來處理單個標識符,以及一個歸納案例以處理多個標識符:
(define-syntax rotate (syntax-rules () [(rotate a) (void)] [(rotate a b c ...) (begin (swap a b) (rotate b c ...))]))
當在一種模式中像c這樣的模式變量被...跟着的時候,它在模板中必須也被...跟着。模式變量有效地匹配一個零序列或多個表,並在模板中以相同的順序被替換。
到目前爲止,rotate的兩種版本都有點效率低下,因爲成對交換總是將第一個變量的值移動到序列中的每個變量,直到達到最後一個變量爲止。更有效的rotate將第一個值直接移動到最後一個變量。我們可以用...模式使用輔助宏去實現更有效的變體:
(define-syntax rotate (syntax-rules () [(rotate a c ...) (shift-to (c ... a) (a c ...))])) (define-syntax shift-to (syntax-rules () [(shift-to (from0 from ...) (to0 to ...)) (let ([tmp from0]) (set! to from) ... (set! to0 tmp))]))
在shift-to宏裏,在模板裏的...後面跟着(set! to from),它導致(set! to from)表達式在to和from序列中與必須使用的每個標識符匹配被複制一樣多次。(to和from匹配的數量必須相同,否則宏擴展就會有一個錯誤的失敗。)
16.1.5 標識符宏
根據我們的宏定義,swap或rotate標識符必須在開括號之後使用,否則會報告語法錯誤:
> (+ swap 3) eval:2:0: swap: bad syntax
in: swap
標識符宏(identifier macro)是一個模式匹配宏,當它被自己使用時不使用括號。例如,我們可以定義val爲一個標識符宏,擴展到(get-val),所以(+ val 3)將擴展到(+ (get-val) 3)。
> (define-syntax val (lambda (stx) (syntax-case stx () [val (identifier? (syntax val)) (syntax (get-val))])))
> (define-values (get-val put-val!) (let ([private-val 0]) (values (lambda () private-val) (lambda (v) (set! private-val v))))) > val 0
> (+ val 3) 3
val宏使用syntax-case,它可以定義更強大的宏,並在《混合模式和表達式:syntax-case》中講解。現在,知道定義宏是必要的,在lambda中使用了syntax-case,它的模板必須用顯式syntax構造器包裝。最後,syntax-case子句可以指定模式後面的附加保護條件。
我們的val宏使用identifier?條件確保在括號中val不能(must not)使用。相反,宏引一個發語法錯誤:
> (val) eval:8:0: val: bad syntax
in: (val)
16.1.6 set!轉化器
使用上面的val宏,我們仍然必須調用put-val!更改存儲值。然而,直接在val上使用set!會更方便。當val用於set!時藉助宏,我們用make-set!-transformer創建一個賦值轉換器(assignment transformer)。我們還必須聲明set!作爲syntax-case文本列表中的文字。
> (define-syntax val2 (make-set!-transformer (lambda (stx) (syntax-case stx (set!) [val2 (identifier? (syntax val2)) (syntax (get-val))] [(set! val2 e) (syntax (put-val! e))])))) > val2 0
> (+ val2 3) 3
> (set! val2 10) > val2 10
16.1.7 宏生成宏
假設我們有許多標識符像val和val2,我們想重定向給訪問器和突變函數像get-val和put-val!。我們希望可以只寫:
(define-get/put-id val get-val put-val!)
自然地,我們可以實現define-get/put-id爲一個宏:
> (define-syntax-rule (define-get/put-id id get put!) (define-syntax id (make-set!-transformer (lambda (stx) (syntax-case stx (set!) [id (identifier? (syntax id)) (syntax (get))] [(set! id e) (syntax (put! e))]))))) > (define-get/put-id val3 get-val put-val!) > (set! val3 11) > val3 11
define-get/put-id宏就是是一個宏生成宏(macro-generating macro)。
16.1.8 擴展的例子:函數的引用調用(Call-by-Reference)
我們可以使用模式匹配宏將一個表添加到Racket中,以定義引用調用函數(call-by-reference function)的一階調用。當通過參考函數本體轉變它的正式參數,這個轉變應用到變量,它在對函數的調用中作爲一個實參提供。
例如,如果define-cbr類似於define,除了定義應用調用函數,那麼
(define-cbr (f a b) (swap a b)) (let ([x 1] [y 2]) (f x y) (list x y))
生成(2 1)。
我們會通過有函數調用支持的對參數的訪問器和轉換器執行參考函數,而不是直接提供參數值。特別是,對於上面的函數f,我們將生成
(define (do-f get-a get-b put-a! put-b!) (define-get/put-id a get-a put-a!) (define-get/put-id b get-b put-b!) (swap a b))
並將函數調用(f x y)重定向到
(do-f (lambda () x) (lambda () y) (lambda (v) (set! x v)) (lambda (v) (set! y v)))
顯然,然後define-cbr是宏生成宏,綁定f到一個宏,它擴展到do-f的一個調用。即(define-cbr (f a b) (swap a b))需要生成的定義
(define-syntax f (syntax-rules () [(id actual ...) (do-f (lambda () actual) ... (lambda (v) (set! actual v)) ...)]))
同時,define-cbr需要使用f本體去定義do-f,第二部分是略微更復雜些,所以我們延遲它的大部分給一個define-for-cbr輔助模塊,它可以讓我們足夠簡單地編寫define-cbr:
(define-syntax-rule (define-cbr (id arg ...) body) (begin (define-syntax id (syntax-rules () [(id actual (... ...)) (do-f (lambda () actual) (... ...) (lambda (v) (set! actual v)) (... ...))])) (define-for-cbr do-f (arg ...) () ; explained below... body)))
我們剩下的任務是定義define-for-cbr以便它轉換
(define-for-cbr do-f (a b) () (swap a b))
到上邊的這個函數定義do-f兩個功能定義。大部分的工作是生成一個define-get/put-id聲明給每個參數,a和b,以及把他們放在本體之前。通常,對於在模式和模板中的...那是很容易的任務,但這次這裏有一個捕獲:我們需要既生成這個名字get-a和put-a!也要生成get-b和put-b!,這個模式語言沒有辦法提供基於現有標識符的綜合標識符。
事實證明,詞法範圍給了我們解決這個問題的方法。訣竅是爲函數中的每個參數迭代一次對define-for-cbr的擴展,這就是爲什麼define-for-cbr開始用一個在參數列表後面明顯無效的()的原因。除了要處理的參數外,我們還需要跟蹤迄今爲止所看到的所有參數以及爲每個生成的get和put名稱。在處理完所有的標識符之後,我們就擁有了所有需要的名稱。
這裏是define-for-cbr的定義:
(define-syntax define-for-cbr (syntax-rules () [(define-for-cbr do-f (id0 id ...) (gens ...) body) (define-for-cbr do-f (id ...) (gens ... (id0 get put)) body)] [(define-for-cbr do-f () ((id get put) ...) body) (define (do-f get ... put ...) (define-get/put-id id get put) ... body)]))
一步一步,展開如下:
(define-for-cbr do-f (a b) () (swap a b)) => (define-for-cbr do-f (b) ([a get_1 put_1]) (swap a b)) => (define-for-cbr do-f () ([a get_1 put_1] [b get_2 put_2]) (swap a b)) => (define (do-f get_1 get_2 put_1 put_2) (define-get/put-id a get_1 put_1) (define-get/put-id b get_2 put_2) (swap a b))
在get_1、get_2、put_1和put_2上的“下標(subscript)”通過宏擴展插入到保留詞法範圍,因爲get被define-for-cbr的每一次迭代生成不應捆綁被不同的迭代生成的get。換句話說,我們本質上欺騙這個宏擴展以生成的我們新的名字,但技術顯示了一些與自動詞法範圍的宏模式的神奇力量。
最後表達式最終擴展成
(define (do-f get_1 get_2 put_1 put_2) (let ([tmp (get_1)]) (put_1 (get_2)) (put_2 tmp)))
它實現了名稱調用(call-by-name)函數f。
接下來,總結一下,我們可以只用三個基於模式的宏添加引用調用(call-by-reference)函數到Racket中:define-cbr、define-for-cbr和define-get/put-id。
16.2 通用宏轉化器
define-syntax表爲標識符創建一個轉換器綁定(transformer binding),這是一個可以在編譯時使用的綁定,同時擴展表達式以在運行時進行求值。與轉換器綁定相關聯的編譯時間值可以是任何東西;如果它是一個參數的過程,則綁定用作宏,而過程是 宏轉換器(macro transformer)。
16.2.1 語法對象
宏轉換器(即源和替換表)的輸入和輸出被表示爲語法對象(syntax object)。語法對象包含符號、列表和常量值(如數字),它們基本上與表達式的quote(引用)表相對應。例如,表達式描述爲(+ 1 2)包含符號'+和數字1和2,都在列表中。除了引用的內容之外,語法對象還將源位置和詞彙綁定信息與表的每個部分關聯起來。在報告語法錯誤時使用源位置信息(例如),詞彙綁定信息允許宏系統維護詞法範圍。爲了適應這種額外的信息,表達式描述爲(+ 1 2)不僅是'(+ 1 2),但'(+ 1 2)的封裝成爲了語法對象。
要創建文字語法對象,請使用syntax表:
> (syntax (+ 1 2)) #<syntax:1:0 (+ 1 2)>
在同樣的方式,'省略了quote,#'省略了syntax:
> #'(+ 1 2) #<syntax:1:0 (+ 1 2)>
只包含符號的語法對象是標識符語法對象(identifier syntax object)。它提供了一些特定於標識符語法對象的附加操作,包括identifier?操作以檢查操作符。最值得注意的是,free-identifier=?確定兩個標識符是否引用相同的綁定:
> (identifier? #'car) #t
> (identifier? #'(+ 1 2)) #f
> (free-identifier=? #'car #'cdr) #f
> (free-identifier=? #'car #'car) #t
> (require (only-in racket/base [car also-car])) > (free-identifier=? #'car #'also-car) #t
要在語法對象中看到列表、符號、數字、etc.等等,請使用syntax->datum:
> (syntax->datum #'(+ 1 2)) '(+ 1 2)
syntax-e函數類似於syntax->datum,但它打開了一個單層的源位置和詞彙上下文信息,離開有它們自己的信息作爲語法的對象子表:
> (syntax-e #'(+ 1 2)) '(#<syntax:1:0 +> #<syntax:1:0 1> #<syntax:1:0 2>)
syntax-e函數總是放棄語法對象包裝器,它包圍着子表,子表表示爲通過符號、數值,和其它文本值。唯一的一次額外的子表打開時展開一個配對,在這種情況下,配對的cdr可以根據語法構建的對象遞歸地展開。
當然,與syntax->datum相對立的是datum->syntax。除了像'(+ 1 2)這樣的數據外,datum->syntax還需要一個現有的語法對象來貢獻它的詞法上下文,並且可以選擇另一個語法對象來貢獻它的源位置:
> (datum->syntax #'lex '(+ 1 2) #'srcloc) #<syntax:1:0 (+ 1 2)>
在上面的例子中,對#'lex的詞法上下文是用於新的語法對象,而#'srcloc源位置被使用。
當datum->syntax的第二個(即,“datum”)參數包含語法對象時,這些語法對象將原封不動地保存在結果中。那就是,用syntax-e解構的結果最終產生了給予datum->syntax的這個語法對象。
16.2.2 宏轉化器程序
任何一個參數的過程都可以是一個宏轉換器(macro transformer)。事實證明,syntax-rules(語法規則)表是一個擴展爲過程表的宏。例如,如果直接求值syntax-rules表(而不是放在define-syntax表的右側),結果就是一個過程:
> (syntax-rules () [(nothing) something]) #<procedure>
可以使用lambda直接編寫自己的宏轉換器過程,而不是使用syntax-rules。對過程的參數是表示源表的語法對象(syntax object),過程的結果必須是表示替換表的語法對象(syntax object):
> (define-syntax self-as-string (lambda (stx) (datum->syntax stx (format "~s" (syntax->datum stx))))) > (self-as-string (+ 1 2)) "(self-as-string (+ 1 2))"
傳遞給宏轉換器的源表表示一個表達式,其中在應用程序位置(即在啓動表達式的括號之後)使用其標識符,或者如果它被用作表達式位置而不是應用程序位置,則它本身代表標識符。
> (self-as-string (+ 1 2)) "(self-as-string (+ 1 2))"
> self-as-string "self-as-string"
define-syntax表支持與define的函數一樣的快捷語法,因此下面的self-as-string定義等同於顯式使用lambda的那個定義:
> (define-syntax (self-as-string stx) (datum->syntax stx (format "~s" (syntax->datum stx)))) > (self-as-string (+ 1 2)) "(self-as-string (+ 1 2))"
16.2.3 混合模式和表達式:syntax-case
通過syntax-rules生成的程序在內部使用syntax-e解構了語法對象,並使用datum->syntax以構造結果。syntax-rules表沒有提供一種方法從模式匹配和模板構建模式中跳轉到任意的Racket表達式中。
syntax-case表允許混合模式匹配、模板構造和任意表達式:
(syntax-case stx-expr (literal-id ...) [pattern expr] ...)
與syntax-rules不同,syntax-case表不產生過程。相反,它從一個stx-expr表達式決定的語法對象來匹配pattern。另外,每個syntax-case有一個pattern和一個expr,而不是一個pattern和template。在一個expr裏,syntax表——通常用#'縮寫——進入模板構造方式;如果一個從句的expr以#'開始,那麼我們就會獲得一些像syntax-rules的表:
> (syntax->datum (syntax-case #'(+ 1 2) () [(op n1 n2) #'(- n1 n2)])) '(- 1 2)
我們可以使用syntax-case來編寫swap宏,以代替define-syntax-rule或syntax-rules:
(define-syntax (swap stx) (syntax-case stx () [(swap x y) #'(let ([tmp x]) (set! x y) (set! y tmp))]))
使用syntax-case的一個優點是,我們可以爲swap提供更好的錯誤報告。例如,使用swap的define-syntax-rule定義,然後(swap x 2)在set!條件中產生語法錯誤!,因爲2不是一個標識符。我們可以改進swap的syntax-case實現來顯式檢查子表:
(define-syntax (swap stx) (syntax-case stx () [(swap x y) (if (and (identifier? #'x) (identifier? #'y)) #'(let ([tmp x]) (set! x y) (set! y tmp)) (raise-syntax-error #f "not an identifier" stx (if (identifier? #'x) #'y #'x)))]))
通過這個定義,(swap x 2)提供了一個源自swap而不是set!的語法錯誤。
在互換上述swap的定義裏,#'x和#'y是模板,即使它們不是作爲宏轉換器的結果。這個例子說明了如何使用模板來訪問輸入語法的片段,在這種情況下可以檢查碎片的表。同時,在對raise-syntax-error的調用中對#'x或#'y的匹配被使用,所以語法錯誤信息可以直接指到非標識符的源位置。
16.2.4 with-syntax和generate-temporaries
由於syntax-case允許我們用任意的Racket表達式進行計算,我們可以更簡單地解決我們在編寫define-for-cbr(參見《擴展的例子:函數的引用調用(Call-by-Reference)》)中的一個問題,在這裏我們需要根據序列id ...生成一組名稱:
(define-syntax (define-for-cbr stx) (syntax-case stx () [(_ do-f (id ...) body) .... #'(define (do-f get ... put ...) (define-get/put-id id get put) ... body) ....]))
代替上面的....我們需要綁定get ...和put ...到生成標識符的列表。我們不能使用let綁定get和put,因爲我們需要綁定那個計數作爲模式變量,而不是普通的局部變量。with-syntax表允許我們綁定模式變量:
(define-syntax (define-for-cbr stx) (syntax-case stx () [(_ do-f (id ...) body) (with-syntax ([(get ...) ....] [(put ...) ....]) #'(define (do-f get ... put ...) (define-get/put-id id get put) ... body))]))
現在我們需要一個表達代替....它生成與在原始模式中匹配id一樣多的標識符。由於這是一個常見的任務,Racket提供了一個輔助函數,generate-temporaries,以一系列的標識符並返回一個序列生成的標識符:
(define-syntax (define-for-cbr stx) (syntax-case stx () [(_ do-f (id ...) body) (with-syntax ([(get ...) (generate-temporaries #'(id ...))] [(put ...) (generate-temporaries #'(id ...))]) #'(define (do-f get ... put ...) (define-get/put-id id get put) ... body))]))
這種方式產生的標識符通常比欺騙宏擴展產生的純粹基於模式的宏的名字更容易理解。
一般來說,一個with-syntax綁定左邊是一個模式,就像在syntax-case中一樣。事實上,一個with-syntax表只是一個syntax-case的部分轉換。
16.2.5 編譯和運行時階段
隨着宏集合越來越多複雜,你可能要寫你自己的輔助函數,如generate-temporaries。例如,提供良好的語法錯誤信息, swap、rotate和define-cbr都應該檢查在源表中的某一個子表是標識符。我們可以使用check-ids函數在任何地方執行此檢查:
(define-syntax (swap stx) (syntax-case stx () [(swap x y) (begin (check-ids stx #'(x y)) #'(let ([tmp x]) (set! x y) (set! y tmp)))])) (define-syntax (rotate stx) (syntax-case stx () [(rotate a c ...) (begin (check-ids stx #'(a c ...)) #'(shift-to (c ... a) (a c ...)))]))
check-ids函數可以使用syntax->list函數將一個語法對象轉換成一個語法對象列表:
(define (check-ids stx forms) (for-each (lambda (form) (unless (identifier? form) (raise-syntax-error #f "not an identifier" stx form))) (syntax->list forms)))
然而,如果以這種方式定義swap和check-ids,則它不會運行:
> (let ([a 1] [b 2]) (swap a b)) check-ids: undefined;
cannot reference undefined identifier
問題是check-ids被定義爲一個運行時表達式,但是swap試圖在編譯時使用它。在交互模式中,編譯時和運行時是交錯的,但它們不是在模塊的主體內交錯的,它們不在預編譯的模塊之間進行交叉。爲了幫助所有這些模式一致地對待代碼,Racket將不同階段的綁定空間分隔開來。
要定義可在編譯時引用的check-ids函數,使用begin-for-syntax:
(begin-for-syntax (define (check-ids stx forms) (for-each (lambda (form) (unless (identifier? form) (raise-syntax-error #f "not an identifier" stx form))) (syntax->list forms))))
使用此語法定義,那麼swap就會運行:
> (let ([a 1] [b 2]) (swap a b) (list a b)) '(2 1)
> (swap a 1) eval:13:0: swap: not an identifier
at: 1
in: (swap a 1)
當將程序組織成模塊時,你也許希望將輔助函數放在一個模塊中,以供駐留在其它模塊上的宏使用。在這種情況下,你可以使用define編寫輔助函數:
"utils.rkt"
#lang racket (provide check-ids) (define (check-ids stx forms) (for-each (lambda (form) (unless (identifier? form) (raise-syntax-error #f "not an identifier" stx form))) (syntax->list forms)))
然後,在實現宏模塊中,使用(require (for-syntax "utils.rkt"))代替(require"utils.rkt")導入輔助函數:
#lang racket (require (for-syntax "utils.rkt")) (define-syntax (swap stx) (syntax-case stx () [(swap x y) (begin (check-ids stx #'(x y)) #'(let ([tmp x]) (set! x y) (set! y tmp)))]))
因爲模塊分別編譯並沒有循環依賴,"utils.rkt"模塊的運行時本體可以在編譯的模塊實現swap編譯。因此,在"utils.rkt"可以用來實現swap,只要他們通過(require (for-syntax ....))明確轉移到編譯時間。
racket模塊提供了syntax-case、generate-temporaries、lambda、if以及更多以用在運行時階段和編譯時階段。這就是爲什麼我們既可以直接在racket的REPL中也可以在一個define-syntax表的右端使用syntax-case的原因。
與此相反,racket/base模塊只在運行時階段導出這些綁定。如果你改變上面定義swap的模塊,使它使用racket/base語言,而不是racket,那麼它不再運行。添加(require(for-syntax racket/base))導入syntax-case和更多進入編譯時階段,使模塊再次工作。
假設define-syntax用於在define-syntax表的右端定義一個局部宏。在這種情況下,內部define-syntax的右端位於元編譯階段等級(meta-compile phase level),也稱爲階段等級2(phase level 2)。要將syntax-case導入到該階段等級,你必須使用(require(for-syntax (for-syntax racket/base))),或者等效地使用(require (for-meta 2racket/base))。例如,
#lang racket/base (require ;; This provides the bindings for the definition ;; of shell-game. (for-syntax racket/base) ;; And this for the definition of ;; swap. (for-syntax (for-syntax racket/base))) (define-syntax (shell-game stx) (define-syntax (swap stx) (syntax-case stx () [(_ a b) #'(let ([tmp a]) (set! a b) (set! b tmp))])) (syntax-case stx () [(_ a b c) (let ([a #'a] [b #'b] [c #'c]) (when (= 0 (random 2)) (swap a b)) (when (= 0 (random 2)) (swap b c)) (when (= 0 (random 2)) (swap a c)) #`(list #,a #,b #,c))])) (shell-game 3 4 5) (shell-game 3 4 5) (shell-game 3 4 5)
反向階段等級也存在。如果一個宏使用了一個導入for-syntax的輔助函數,如果輔助函數返回由syntax生成的語法對象常量,那麼語法中的標識符將需要在階段等級-1(phase level -1),也稱爲模板階段等級(template phase level),以便在運行時階段等級相對於定義宏的模塊有任何綁定。
例如,在下面的例子中沒有語法變換器的swap-stx的輔助函數——它只是一個普通的函數——但它產生的語法對象得到拼接成shell-game的結果。因此,其包含的輔助模塊需要在shell-game階段1用(require (for-syntax 'helper))導入。
但從swap-stx的角度,當被shell-game返回的語法求值時,其結果最終在階段1求值。換句話說,一個負向階段等級是一個從正方向來看的相反的方階段等級:shell-game的階段1是swap-stx的階段0,所以shell-game的階段0是swap-stx的階段-1。這就是爲什麼這個例子不運行——'helper子模塊在階段-1沒有綁定。
#lang racket/base (require (for-syntax racket/base)) (module helper racket/base (provide swap-stx) (define (swap-stx a-stx b-stx) #`(let ([tmp #,a-stx]) (set! #,a-stx #,b-stx) (set! #,b-stx tmp)))) (require (for-syntax 'helper)) (define-syntax (shell-game stx) (syntax-case stx () [(_ a b c) #`(begin #,(swap-stx #'a #'b) #,(swap-stx #'b #'c) #,(swap-stx #'a #'c) (list a b c))])) (define x 3) (define y 4) (define z 5) (shell-game x y z)
修復這個例子,我們添加(require (for-template racket/base))到'helper子模塊。
#lang racket/base (require (for-syntax racket/base)) (module helper racket/base (require (for-template racket/base)) ; binds `let` and `set!` at phase -1 (provide swap-stx) (define (swap-stx a-stx b-stx) #`(let ([tmp #,a-stx]) (set! #,a-stx #,b-stx) (set! #,b-stx tmp)))) (require (for-syntax 'helper)) (define-syntax (shell-game stx) (syntax-case stx () [(_ a b c) #`(begin #,(swap-stx #'a #'b) #,(swap-stx #'b #'c) #,(swap-stx #'a #'c) (list a b c))])) (define x 3) (define y 4) (define z 5) (shell-game x y z) (shell-game x y z) (shell-game x y z)
16.2.6 通用階段等級
一個階段(phase)可以被看作是在一個進程的管道中分離出計算的方法,在這個過程中,一個代碼生成下一個程序所使用的代碼。(例如,由預處理器進程、編譯器和彙編程序組成的管道)。
設想爲此啓動兩個Racket過程。如果忽略套接字和文件之類的進程間通信通道,則進程將無法共享任何其它內容,而不是從一個進程的標準輸出導入到另一個進程的標準輸入中的文本。同樣,Racket有效地允許一個模塊的多個調用在同一進程中存在但相隔階段。Racket強制分離這些階段,不同的階段不能以任何方式進行通信,除非通過宏擴展協議,其中一個階段的輸出是下一階段使用的代碼。
16.2.6.1 階段和綁定
一個標識符的每個綁定都存在於特定的階段中。一個綁定和它的階段之間的鏈接用整數階段等級(phase level)表示。階段等級0是用於“扁平”(或“運行時”)定義的階段,因此
(define age 5)
爲age添加綁定到階段等級0中。標識符age可以在較高的階段等級上用begin-for-syntax定義:
(begin-for-syntax (define age 5))
對一個單一的begin-for-syntax包裝器,age被定義在階段等級1。我們可以容易地在同一個模塊或頂級命名空間中混合這兩個定義,並且在不同的階段等級上定義的兩個age之間沒有衝突:
> (define age 3)
> (begin-for-syntax (define age 9))
在階段等級0的age綁定有一個值爲3,在階段等級1的age綁定有一個值爲9。
語法對象將綁定信息捕獲爲一級值。因此,
#'age
是一個表示age綁定的語法對象,但因爲有兩個age(一個在階段等級0,一個在階段等級1),哪一個是它捕獲的?事實上,Racket用給所有階段等級的詞彙信息充滿#'age,所以答案是#'age捕捉兩者。
當#'age被最終使用時,被#'age捕獲的age的相關的綁定被決定。作爲一個例子,我們將#'age綁定到模式變量,所以我們可以在一個模板裏使用它,並對模板求值eval:
> (eval (with-syntax ([age #'age]) #'(displayln age))) 3
結果是3,因爲age在第0階段等級使用。我們可以在begin-for-syntax內再試一次age的使用:
> (eval (with-syntax ([age #'age]) #'(begin-for-syntax (displayln age)))) 9
在這種情況下,答案是9,因爲我們使用的age是階段等級1而不是0(即, begin-for-syntax在階段等級1求值表達式)。所以,你可以看到,我們用相同的語法對象開始,#'age,並且我們能夠使用這兩種不同的方法:在階段等級0和在階段等級1。
語法對象有一個詞法上下文,從它第一次存在的時刻起。從模塊中提供的語法對象保留其詞法上下文,因此它引用源模塊上下文中的綁定,而不是其使用上下文中的。下面的示例在第0階段等級定義了button,並將其綁定到0,同時see-button爲在模塊a中的button綁定語法對象:
> (module a racket (define button 0) (provide (for-syntax see-button)) ; Why not (define see-button #'button)? We explain later. (define-for-syntax see-button #'button))
> (module b racket (require 'a) (define button 8) (define-syntax (m stx) see-button) (m)) > (require 'b) 0
在m宏的結果是see-button的值,它是帶有模塊a的詞彙上下文的#'button。即使是在b中的另一個button,第二個button不會混淆Racket,因爲#'button(這個值對see-button予以綁定)的詞彙上下文是a.
注意,see-button是被用define-for-syntax定義它的長處約束在第1階段等級。由於m是一個宏,所以需要第1階段等級,所以它的本體在高於它的定義上下文的一個階段執行。由於m是在第0階段等級定義的,所以它的本體處於階段等級1,所以由本體引用的任何綁定都必須在階段等級1上。
16.2.6.2 階段和模塊
階段等級(phase level)是一個模塊相關概念。當通過require從另一個模塊導入時,Racket允許我們將導入的綁定轉移到與原來的綁定不同的階段等級:
(require "a.rkt") ; 不帶階段轉移的導入 (require (for-syntax "a.rkt")) ; 通過+1轉移階段 (require (for-template "a.rkt")) ; 通過-1轉移階段 (require (for-meta 5 "a.rkt")) ; 通過+5轉移階段
也就是說,在require中使用for-syntax意味着該模塊的所有綁定都會增加它們的階段等級。在第0階段等級define(定義)並用for-syntax導入的綁定成爲一個階段等級1的綁定:
> (module c racket (define x 0) ; 在0階段等級定義 (provide x))
> (module d racket (require (for-syntax 'c)) ; 在階段等級1有一個綁定,不是0: #'x)
讓我們看看如果我們試圖在階段等級0創建一個給#'button綁定的語法對象發生了什麼:
> (define button 0) > (define see-button #'button)
現在button和see-button在第0階段等級中定義。對#'button的詞彙上下文會知道在階段等級0有一個爲button的綁定。事實上,好像如果我們試圖對see-button進行eval,一切都很好地運行:
> (eval see-button) 0
現在,讓我們在一個宏中使用see-button:
> (define-syntax (m stx) see-button) > (m) see-button: undefined;
cannot reference undefined identifier
顯然,see-button沒有在階段1級被定義,所以我們不能在宏本體內引用它。讓我們在另一個模塊中使用see-button,通過將button定義放到一個模塊中並在第1階段等級導入它。那麼,我們在第1階段等級得到了see-button:
> (module a racket (define button 0) (define see-button #'button) (provide see-button))
> (module b racket (require (for-syntax 'a)) ; 在階段等級1取得see-button (define-syntax (m stx) see-button) (m)) eval:1:0: button: unbound identifier;
also, no #%top syntax transformer is bound
in: button
Racket說button現在不受約束!當a在階段等級1被導入,我們有了以下的綁定:
button 在階段等級1 see-button 在階段等級11
所以宏m能夠看到一個在階段等級1爲see-button的綁定並且將返回#'button語法對象,它指的是在階段等級1的button綁定。但是m的使用是在第0階段等級,在b的第0階段等級沒有button。這就是爲什麼see-button需要約束在第1階段等級,就像在原來的a中。那麼,在原來的b中,我們有以下綁定:
button 在階段等級0 see-button 在階段等級1
在這個場景中,我們可以在宏中使用see-button,因爲在階段等級1上see-button是綁定的。當宏展開時,它將指向一個在第0階段等級的button綁定。
用(define see-button #'button)定see-button本身沒有錯;它取決於我們打算如何使用see-button。例如,我們可以安排m明智地使用see-button,因爲它使用begin-for-syntax將它放在了階段等級1的上下文中:
> (module a racket (define button 0) (define see-button #'button) (provide see-button))
> (module b racket (require (for-syntax 'a)) (define-syntax (m stx) (with-syntax ([x see-button]) #'(begin-for-syntax (displayln x)))) (m)) 0
在這種情況下,模塊b在階段等級1上機有button綁定也有see-button綁定。宏的擴展是
(begin-for-syntax (displayln button))
它的工作原理是,因爲button在階段等級1上綁定。
現在,你可以通過在階段等級0和階段等級1中導入a來欺騙階段系統。然後,你將具有以下綁定
button 在階段等級0 see-button 在階段等級0 button 在階段等級1 see-button 在階段等級1
你可能現在希望宏中的see-button可以工作,但它沒有:
> (module a racket (define button 0) (define see-button #'button) (provide see-button))
> (module b racket (require 'a (for-syntax 'a)) (define-syntax (m stx) see-button) (m)) eval:1:0: button: unbound identifier;
also, no #%top syntax transformer is bound
in: button
宏m中的see-button來自於(for-syntax 'a)的導入。爲了讓宏m運行,它需要在第0階段有button綁定。那個綁定存在——它通過(require 'a)表明。然而,(require 'a)和(require (for-syntax 'a))是相同模塊的不同實例(different instantiation)。第1階段的see-button按鈕僅指第1階段的button,而不是來自一個不同實例的第0階段綁定的button,即使來自同一個源模塊。
這種階段等級之間的不匹配的實例可以用syntax-shift-phase-level修復。像#'button捕捉詞彙信息那樣在所有(all)階段等級重調用語法對象。這裏的問題是see-button在第1階段調用,但需要返回一個可以在第0階段進行求值的語法對象。默認情況下,see-button在同一階段綁定到#'button。但通過syntax-shift-phase-level,我們可以使see-button在不同的相對階段等級適用於#’button。在這種情況下,我們使用一個-1的階段轉移使在階段1的see-button適用於在0階段的#'button。(由於階段轉變發生在每一個等級,它也使0階段的see-button適用於-1階段的#'button)
注意,syntax-shift-phase-level只創建跨階段的引用。爲了使該引用運行,我們仍然需要在兩個階段實例化我們的模塊,以便引用和它的目標具有可用的綁定。因此,在模塊'b,我們仍然在階段0和階段1導入模塊'a——使用(require 'a (for-syntax 'a))——因此我們爲see-button的階段1綁定和爲button的一個階段0綁定。現在宏m就會運行了。
> (module a racket (define button 0) (define see-button (syntax-shift-phase-level #'button -1)) (provide see-button))
> (module b racket (require 'a (for-syntax 'a)) (define-syntax (m stx) see-button) (m)) > (require 'b) 0
順便問一下,在第0階段綁定的see-button會發生什麼?其#’button綁定也已經轉移,但是對階段-1。由於button本身在-1階段不受約束,如果我們試圖在第0階段對see-button求值,我們會得到一個錯誤。換句話說,我們並沒有永久地解決我們的不匹配問題——我們只是把它轉移到一個不太麻煩的位置。
> (module a racket (define button 0) (define see-button (syntax-shift-phase-level #'button -1)) (provide see-button))
> (module b racket (require 'a (for-syntax 'a)) (define-syntax (m stx) see-button) (m))
> (module b2 racket (require 'a) (eval see-button)) > (require 'b2) button: undefined;
cannot reference undefined identifier
當宏試圖匹配字面綁定時——使用syntax-case或syntax-parse,也可能出現上述不匹配的錯誤。
> (module x racket (require (for-syntax syntax/parse) (for-template racket/base)) (provide (all-defined-out)) (define button 0) (define (make) #'button) (define-syntax (process stx) (define-literal-set locals (button)) (syntax-parse stx [(_ (n (~literal button))) #'#''ok])))
> (module y racket (require (for-meta 1 'x) (for-meta 2 'x racket/base)) (begin-for-syntax (define-syntax (m stx) (with-syntax ([out (make)]) #'(process (0 out))))) (define-syntax (p stx) (m)) (p)) eval:2.0: process: expected the identifier `button'
at: button
in: (process (0 button))
在這個例子中,在階段等級2裏make正在y中被使用,它返回的#'button語法對象——指的是button在階段0的x裏的綁定和在階段等級2的來自(for-meta 2 'x)的y裏的綁定。process宏在階段等級1從(for-meta 1 'x)導入,並且它知道button應該在階段等級1上綁定。當syntax-parse在process內執行時,它正在尋找在階段等級1上綁定的button,但它僅看到一個階段等級2綁定並且不匹配。
爲了修正這個例子,我們可以相對於x在階段等級1提供make,然後在y中的第1階段等級導入它:
> (module x racket (require (for-syntax syntax/parse) (for-template racket/base)) (provide (all-defined-out)) (define button 0) (provide (for-syntax make)) (define-for-syntax (make) #'button) (define-syntax (process stx) (define-literal-set locals (button)) (syntax-parse stx [(_ (n (~literal button))) #'#''ok])))
> (module y racket (require (for-meta 1 'x) (for-meta 2 racket/base)) (begin-for-syntax (define-syntax (m stx) (with-syntax ([out (make)]) #'(process (0 out))))) (define-syntax (p stx) (m)) (p)) > (require 'y) 'ok
16.2.7 語法污染
一個宏的一個使用可以擴展到一個標識符的使用,該標識符不會從綁定宏的模塊中導出。一般來說,這樣的標識符不必從擴展表達式中提取出來,並在不同的上下文中使用,因爲使用不同上下文中的標識符可能會中斷宏模塊的不變量。
例如,下面的模塊導出一個宏go,它擴展到unchecked-go的使用:
"m.rkt"
#lang racket (provide go) (define (unchecked-go n x) ; to avoid disaster, n must be a number (+ n 17)) (define-syntax (go stx) (syntax-case stx () [(_ x) #'(unchecked-go 8 x)]))
如果對unchecked-go的引用從(go 'a)擴展解析,那麼它可能會被插入一個新的表達,(unchecked-go #f 'a),導致災難。datum->syntax程序同樣可用於構建一個導出標識符引用,即使沒有宏擴展包括一個對標識符的引用。
爲了防止這種濫用的導出標識符,這個宏go必須用syntax-protect明確保護其擴展:
(define-syntax (go stx) (syntax-case stx () [(_ x) (syntax-protect #'(unchecked-go 8 x))]))
syntax-protect函數會導致從go被污染(tainted)的結果中提取的任何語法對象。宏擴展程序拒絕受污染的標識符,因此試圖從(go 'a)的擴展中提取unchecked-go產生一個標識符,該標識符不能用於構造一個新表達式(或者,至少,不是宏擴展程序將接受的表達式)。syntax-rules、syntax-id-rule和define-syntax-rule表自動保護其擴展結果。
更確切地說,syntax-protect 配備了一個帶一個染料包(dye pack)的語法對象。當一個語法對象被配備時,那麼syntax-e在它的結果污染任何語法對象。同樣,它的當第一個參數被配備時,datum->syntax污染其結果。最後,如果引用的語法對象的任何部分被配備,則相應的部分將受到所生成的語法常數的影響。
當然,宏擴展本身必須能夠解除(disarm)語法對象上的污染,以便它可以進一步擴展表達式或其子表達式。當一個語法對象配備有一個染料包時,染料包裝有一個相關的檢查者,可以用來解除染料包裝。一個(syntax-protect stx)函數調用實際上是一個對(syntax-arm stx #f #t)的簡寫,這配備stx使用合適的檢查程序。在試圖擴展或編譯它之前,擴展程序使用syntax-disarm並在每個表達式上使用它的檢查程序。
與宏擴展程序從語法轉換器的輸入到其輸出的屬性(參見《(part ("(lib scribblings/reference/reference.scrbl)" "stxprops"))》(Syntax Object Properties))相同,擴展程序將從轉換器的輸入複製染料包到輸出。以前面的例子爲基礎,
"n.rkt"
#lang racket (require "m.rkt") (provide go-more) (define y 'hello) (define-syntax (go-more stx) (syntax-protect #'(go y)))
(go-more)的擴展介紹了一個對在(go y)中的非導出y的引用,以及擴展結果被裝備,這樣y不能從擴展中提取。即使go沒有爲其結果使用syntax-protect(可能歸根到底是因爲它不需要保護unchecked-go),(go y)上的染色包被傳播給了最終擴展(unchecked-go 8y)。宏擴展器使用syntax-rearm從轉換程序的輸入和輸出增殖(propagate)染料包。
16.2.7.1 污染模式
在某些情況下,一個宏執行者有意允許有限的解構的宏結果沒有污染結果。例如,給出define-like-y宏,
"q.rkt"
#lang racket (provide define-like-y) (define y 'hello) (define-syntax (define-like-y stx) (syntax-case stx () [(_ id) (syntax-protect #'(define-values (id) y))]))
也有人可以在內部定義中使用宏:
(let () (define-like-y x) x)
"q.rkt"模塊的執行器最有可能是希望允許define-like-y這樣的使用。以轉換一個內部定義爲letrec綁定,但是通過define-like-y產生的define表必須解構,這通常會污染x的綁定和對y的引用。
相反,對define-like-y的內部使用是允許的,因爲syntax-protect特別對待一個以define-values開始的語法列表。在這種情況下,代替裝備整個表達式的是,語法列表中的每個元素都被裝備,進一步將染料包推到列表的第二個元素中,以便它們被附加到定義的標識符上。因此,在擴展結果(define-values (x) y)中的define-values、x和y分別被裝備,同時定義可以被解構以轉化爲letrec。
就像syntax-protect,通過將染料包推入這個列表元素,這個擴展程序重新裝備一個以define-values開始的轉換程序結果。作爲一個結果, define-like-y已經實施產生(define id y),它使用define代替define-values。在這種情況下,整個define表首先裝備一個染料包,但是一旦define表擴展到define-values,染料包被移動到各個部分。
宏擴展程序以它處理以define-values開始的結果相同的方式處理以define-syntaxes開始的語法列表結果。從begin開始的語法列表結果同樣被處理,除了語法列表的第二個元素被當作其它元素一樣處理(即,直接元素被裝備,而不是它的上下文)。此外,宏擴展程序遞歸地應用此特殊處理,以防宏生成包含嵌套define-values表的一個begin表。
通過將一個'taint-mode屬性(見《(part ("(lib scribblings/reference/reference.scrbl)" "stxprops"))》(Syntax Object Properties))附加到宏轉換程序的結果語法對象中,可以覆蓋染料包的默認應用程序。如果屬性值是'opaque,那麼語法對象被裝備而且不是它的部件。如果屬性值是'transparent,則語法對象的部件被裝備。如果屬性值是'transparent-binding,那麼語法對象的部件和第二個部件的子部件(如define-values和define-syntaxes)被裝備。'transparent和'transparent-binding模式觸發遞歸屬性在部件的檢測,這樣就可以把裝備任意深入地推入到轉換程序的結果中。
16.2.7.2 污染和代碼檢查
想要獲得特權的工具(例如調試轉換器)必須在擴展程序中解除染料包的作用。權限是通過 代碼檢查器(code inspector)授予的。每個染料包的記錄一個檢查器,同時語法對象可以使用使用一個足夠強大的檢查器解除。
當聲明一個模塊時,該聲明將捕獲current-code-inspector參數的當前值。當模塊中定義的宏轉換器應用syntax-protect時,將使用捕獲的檢查器。一個工具可以通過提供與一個相同的檢查器或模塊檢查器的超級檢查器提供syntax-disarm對結果語法對象予以解除。在將current-code-inspector設置爲不太強大的檢查器(在加載了受信任的代碼,如調試工具,之後),最終會運行不信任代碼。
有了這種安排,宏生成宏需要小心些,因爲正在生成的宏可以在已經生成的宏中嵌入語法對象,這些已經生成的宏需要正在生成的模塊的保護等級,而不是包含已經生成的宏的模塊的保護等級。爲了避免這個問題,使用模塊的聲明時間檢查器,它是可以作爲(variable-reference->module-declaration-inspector (#%variable-reference))訪問的,並使用它來定義一個syntax-protect的變體。
例如,假設go宏是通過宏實現的:
#lang racket (provide def-go) (define (unchecked-go n x) (+ n 17)) (define-syntax (def-go stx) (syntax-case stx () [(_ go) (protect-syntax #'(define-syntax (go stx) (syntax-case stx () [(_ x) (protect-syntax #'(unchecked-go 8 x))])))]))
當def-go被用於另一個模塊定義go時,並且當go定義模塊處於與def-go定義模塊不同的保護等級時,生成的protect-syntax的宏使用是不正確的。在unchecked-go在def-go定義模塊等級應該被保護,而不是go定義模塊。
解決方案是定義和使用go-syntax-protect,而不是:
#lang racket (provide def-go) (define (unchecked-go n x) (+ n 17)) (define-for-syntax go-syntax-protect (let ([insp (variable-reference->module-declaration-inspector (#%variable-reference))]) (lambda (stx) (syntax-arm stx insp)))) (define-syntax (def-go stx) (syntax-case stx () [(_ go) (protect-syntax #'(define-syntax (go stx) (syntax-case stx () [(_ x) (go-syntax-protect #'(unchecked-go 8 x))])))]))
16.2.7.3 受保護的導出
有時,一個模塊需要將綁定導出到一些模塊——其它與導出模塊在同一信任級別上的模塊——但阻止不受信任模塊的訪問。此類導出應使用provide中的protect-out表。例如,ffi/unsafe導出所有的非安全綁定作爲從這個意義上講的受保護的(protected)。
代碼檢查器再次提供了這個機制,它確定哪一個模塊是可信的以及哪一個模塊是不可信的。當一個模塊被聲明時,current-code-inspector的值被關聯到模塊聲明。當一個模塊被實例化時(即當聲明的主體實際上被執行了),一個子檢查器被創建來保護模塊的導出。對模塊的受保護的(protected)導出的訪問需要一個在檢查器層次結構上比這個模塊的實例化檢查器更高級別的代碼檢查器;注意一個模塊的聲明檢查器總是高於它的實例化檢查器,因此模塊以相同的代碼聲明檢查器可以訪問其它每一個的導出。
在一個模塊中的語法對象常量,如在一個模板中的文字標識符,保留它們的源模塊的檢查器。以這方式,來自於一個受信任的模塊的一個宏可以在不可信的模塊內使用,同時宏擴展中的受保護的(protected)標識符一直在工作,即使通過它們最終出現在不可信的模塊中。當然,這樣的標識符應該被裝備(arm),所以它們不能從宏擴展中提取並被非信任代碼濫用。
不幸的是,因爲它可以用除了編譯(compile)之外的其它方法合成,因此來自於一個".zo"文件的被編譯代碼本質上是不可信的。當編譯後的代碼寫入到一個".zo"文件中,編譯代碼中的語法對象常量就失去了檢查器。當代碼加載時,已編譯代碼中的所有語法對象常量獲得封閉模塊的聲明時間檢查器。