Scheme 程式設計/宏
據說宏是 Lisp 成為 Lisp 的原因。其他程式語言,特別是 C 和 C++,也有宏,但它們不像 Lisp 宏。在 C 中,宏預處理器允許您對 C 程式碼片段進行有限的文字替換。有時,這會產生有效的 C 程式碼,而其他時候則會導致編譯器難以確定的隱形錯位括號或分號。
Lisp 宏是成熟的 Lisp 過程,具有任何其他 Lisp 過程的全部功能。它們不是文字,而是接收表示要更改的程式碼片段的列表。宏的返回值是一個表示新程式的列表。Scheme 支援三種類型的宏。在理解正在發生的事情方面,這三種宏中最容易理解的是 Lisp 最初的宏型別,在 Common Lisp、EMACS Lisp 和 SCM 中被稱為 defmacro。其他 Scheme 實現將這種形式稱為 define-macro,而其他一些實現,特別是 Racket,則將其視為過於原始而拒絕使用。
要在 SCM 中使用宏,您必須首先評估此形式
(require 'macro)
其他 Scheme 實現通常不需要此步驟。
一個簡單的宏可以說明正在發生的事情。我們希望它做一些用過程無法做到的事情。因此,我們將實現一個像 begin 形式一樣的宏,但按相反的順序執行其中的程式碼
(defmacro (backwards . body)
(cons 'begin
(reverse body)))
每次您寫下這個
(backwards
(newline)
(display 3)
(newline)
(display 2)
(newline)
(display 1))
...上面的宏程式碼將在編譯時被呼叫,backwards 塊內的程式碼將作為 body 引數傳遞。backwards 宏反轉此程式碼並將符號 begin 插入結果的開頭,建立一個 begin 形式
(begin
(display 1)
(newline)
(display 2)
(newline)
(display 3)
(newline))
您的程式將執行,就好像該 begin 形式是您在程式中編寫的。宏可以對引數中的程式碼做任何事情。這是一個更有用的宏
(defmacro (while condition . body)
`(let loop ()
(cond (,condition
(begin . ,body)
(loop)))))
然後你可以寫這個程式
(define counter 10)
(while (> counter 0)
(display counter)
(newline)
(set! counter (- counter 1)))
while 迴圈檢查條件表示式是否為真,如果是,則重複執行迴圈體,直到條件變為假。在上面的程式碼中,當 counter 達到 0 時,條件變為假。
defmacro 只有一個小小的問題。當您使用 while 迴圈時,迴圈體中的程式碼可以訪問 loop 過程,而該過程是我們的 while 迴圈使用的遞迴過程。您可以將其設定為其他內容,然後迴圈將中斷。您可能會意外地這樣做
> (while (< counter 10)
(display counter)
(newline)
(set! loop 'oops))
0
;ERROR: Wrong type to apply: oops
; in expression: (loop)
; in scope:
; () procedure loop
; (loop . #@let)
;STACK TRACE
1; ((#@cond ((#@< #@counter 10) (#@display #@counter) (#@newline) ...
2; ((#@let ((loop (#@lambda () (#@cond ((#@< #@counter 10) (#@dis ...
Lisp 提供了一個解決方案,在 Common Lisp 和大多數 Scheme 實現中稱為 gensym。然而,在 SCM 中,此過程稱為 gentemp。它生成一個符號,保證在程式的其他任何地方都不會出現。您可以使用它生成一個名稱來代替 loop。修復 while 宏,它現在看起來像這樣
(defmacro (while condition . body)
(let ((loop (gentemp))) ; gensym if you're not using SCM.
`(let ,loop ()
(cond (,condition
(begin . ,body)
(,loop))))))
Scheme 將衛生宏引入 Lisp 世界。在編寫這種宏時,不可能意外地引入您正在修改的程式碼可以更改的變數名。但是,要編寫它們,我們必須放棄程式碼由簡單列表構成的概念。
有兩種型別的衛生宏,但 SCM 只支援其中功能較弱的一種。Syntax-rules 宏依賴於模式匹配來定義呼叫宏的有效方法,以及模板來定義輸出程式碼。有一些宏無法用 syntax-rules 定義。以下是使用 syntax-rules 而不是 defmacro 定義的 while 迴圈
(define-syntax while
(syntax-rules ()
((_ condition . body)
(let loop ()
(cond (condition
(begin . body)
(loop)))))))
如果您在這個版本的 while 的迴圈體內引用 loop,它指的是與模板中顯示的 loop 不同的 loop。無法使用 syntax-rules 定義宏外部的程式碼可以看見的變數。
(_ condition . body) 形式是模式。_ 是萬用字元。任何東西都可以匹配它,它的值不會繫結到任何東西。在本例中,它位於 while 所在的位置。condition 和 body 是模式變數。它們不是真正的變數。它們在執行時不會有值,而只在模板內部有值。由於點列表表示法,body 匹配在 cdr 位置,這意味著它是在 body 之後的所有形式。
Scheme 的 cond 形式可以使用此宏系統實現。在大多數實現中,cond 事實上是一個宏。典型的 cond 定義如下
(define-syntax my-cond
(syntax-rules (else)
((_) (if #f 'not-reached))
((_ (else . body))
(begin . body))
((_ (bool-expression . body) . rest)
(if bool-expression
(begin . body)
(cond . rest)))))
my-cond 的工作原理與 Racket 版本的 cond 相同,即 (cond) 是合法的,並且具有未指定的返回值,並且與 SCM 版本的 cond 不同,在 SCM 版本的 cond 中,(cond) 是不合法的。括號中的頂部的 (else) 是字面量列表。它們必須與程式碼中的內容完全匹配才能匹配模式。換句話說,在模式中看到 else 的地方,只有在該處找到 else 時才會匹配。
如果您可以在執行時以與使用 syntax-rules 時在編譯時匹配語法相同的方式匹配正則列表,那麼,如果您有一個列表,並且您想捕獲前兩個元素(或者如果它不是真正的列表,或者如果它沒有兩個元素,則執行其他操作),您可以編寫類似以下內容
(match some-value (literally-hitler)
((literally-hitler . rest) ; First element is literally Hitler.
(error "Found the Nazi"))
(((a b) second . rest) ; First element is a two-element list.
(display a))
((first second . rest) ; It's a list with at least two elements.
(display (list first second)))
(else #f))
宏支援字面量,就像 syntax-rules 一樣。唯一不支援的是萬用字元運算子。每個匹配的值都將繫結到某些內容。
我們將使用 syntax-rules,因為最終,它提供的模式匹配比 defmacro 的圖靈完備性更能簡化這項工作。完成後,我們就可以在 defmacro 宏中使用相同的模式匹配,方法是在其主體中使用此宏。defmacro 中唯一缺少的是衛生。
使用 syntax-rules 實現類似的東西需要兩個宏。一個宏將匹配單個模式,並在成功時評估表示式,或者在失敗時返回失敗值。
match-pattern 宏需要五個引數
(match-pattern pattern literals match-value fail-value success-expr)
pattern 包含希望在一些候選結構中找到它們的位置的變數名稱。因此,例如,如果模式是 (a . b),那麼您試圖將 a 繫結到列表的 car,將 b 繫結到它的 cdr。如果 a 和 b 不是字面量,那麼可以透過生成以下程式碼來將此模式與 match-value 匹配
(let ((hd (maybe-car match-value fail-value)))
(if (eq? hd fail-value)
fail-value
(match-pattern tl literals (maybe-cdr match-value fail-value)
fail-value success-expr))))
maybe-car 是 car 的一個特殊版本,如果它在非對上呼叫,則不會引發錯誤。maybe-car 不像假設 match-value 將是一個列表,它使我們能夠進行檢查,而無需每次都編寫 (if (pair? match-value) ...)。
如果列表的 car 匹配,則會再次在資料的 cdr 和模式的 cdr(上面的程式碼片段中的 tl)上使用 match-pattern 宏。這樣,tl 可以是另一個模式,也可以是一個變數。
如果 maybe-car 或 maybe-cdr 遇到不是列表的內容,它們將返回 fail-value,我們將在其中進行檢查,如果找到它,則返回它。這樣,如果對 maybe-car 或 maybe-cdr 的任何呼叫都評估為 fail-value,那麼整個 match-pattern 也將評估為 fail-value。
宏將使用來自 迴圈 章節的 exists-in? 來實現字面量。如果在 pattern 中找到 literals 中的識別符號,則該識別符號將被解釋為符號,並且結構中的該位置必須是該識別符號,否則將返回 fail-value。failure-value 將從宏外部提供。最終,我們將在執行時使用 (gentemp)(或 (gensym))在除 SCM 之外的任何 Scheme 實現上生成失敗值。
(require 'macro)
(define (maybe-car obj fail-value)
(if (pair? obj)
(car obj)
fail-value))
(define (maybe-cdr obj fail-value)
(if (pair? obj)
(cdr obj)
fail-value))
(define exists-in?
(lambda (ele lis)
(cond ((null? lis) #f)
((equal? ele (car lis)) #t)
(else (exists-in? ele (cdr lis))))))
(define-syntax match-pattern
(syntax-rules ()
;; No pattern. Matches if the match-value is null.
((_ () literals match-value fail-value success-expr)
(if (null? match-value)
success-expr
fail-value))
;; Notice there are TWO pattern-matches going on: One at compile-time via
;; syntax-rules, and another at runtime, being done with cond forms
;; and comparison with the 'fail-value to detect failures deeper in the
;; pattern.
;;
;; This case matches when the first element of the pattern is a list.
;; It generates code that matches the match-value only if its first element
;; is also a list.
((_ ((hhd . htl) . tl) literals match-value fail-value success-expr)
(cond ((eq? match-value fail-value)
fail-value)
;; Macros are allowed to expand into instances of themselves.
(else (match-pattern (hhd . htl) literals (maybe-car match-value fail-value)
fail-value
(match-pattern tl literals (maybe-cdr match-value fail-value) fail-value
success-expr)))))
;; Matches if the pattern itself is a list. hd, short for "head", is a
;; variable that will be bound to the first element of the match-value if it's
;; a list. If it's not a list, (maybe-car) will cause hd to be bound to the fail-value.
;;
;; Also, the match-value may already be the fail-value due to occurrences at a shallower
;; level in the pattern. If this happens, then this code won't bother to delve any deeper.
((_ (hd . tl) literals match-value fail-value success-expr)
(cond ((eq? match-value fail-value)
fail-value)
((exists-in? 'hd 'literals)
(if (eq? (maybe-car match-value fail-value) 'hd)
(match-pattern tl literals (maybe-cdr match-value fail-value)
fail-value success-expr)
fail-value))
(else
(let ((hd (maybe-car match-value fail-value)))
(if (eq? hd fail-value)
fail-value
(match-pattern tl literals (maybe-cdr match-value fail-value)
fail-value success-expr))))))
;; The pattern doesn't have to be a list. If it's not, it'll be bound to the
;; whole match-value. Control can also reach here if the non-list pattern
;; is in the cdr position of a larger pattern.
((_ non-list literals match-value fail-value success-expr)
(cond ((eq? match-value fail-value)
fail-value)
((exists-in? 'non-list 'literals)
(if (eq? 'non-list match-value)
success-expr
fail-value))
(else (let ((non-list match-value)) success-expr))))))
此示例顯示了它的作用
> (define test '((a 2) (3 4)))
#<unspecified>
> (match-pattern ((a b) (c d)) (a) test 'fail (list b c d))
(2 3 4)
> (define test '((1 2) (3 4)))
#<unspecified>
> (match-pattern ((a b) (c d)) (a) test 'fail (list b c d))
fail
第二種情況失敗是因為 a 被定義為一個字面量,因此必須在其中有一個實際的 a 符號才能匹配模式。
失敗值使我們能夠在 cond 形式中評估這些值的鏈,這使得我們可以編寫一個映象 syntax-rules 的多模式匹配宏。
(define-syntax match
(syntax-rules (else)
((_ value literals) (error "No patterns matched"))
((_ value literals (else . body))
(begin . body))
((_ value literals (pattern . body) . rest)
(let* ((fail (gentemp))
(result (match-pattern pattern literals value fail
(begin . body))))
(if (eq? result fail)
(match value literals . rest)
result)))))
一個例子
(match '((1 2) (3 4)) (a)
(((a b) (c d))
(list b c d))
(((b c) (d e))
(list (+ b c) (+ d e)))
(else 'whatever))
為了演示宏與defmacro一起使用的用法,讓我們再次使用defmacro定義my-cond。
(defmacro (my-cond . clauses)
(match (cons 'my-cond clauses) (else)
((x) '(if #f 'not-reached))
((x (else . body))
`(begin . ,body))
((x (bool-expression . body) . rest)
`(if ,bool-expression
(begin . ,body)
(my-cond . ,rest)))))
這就是宏的功能。