跳转到内容

Scheme 编程/宏

来自 Wikibooks,开放世界中的开放书籍

据说宏是 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))))))

卫生宏:syntax-rules

[编辑 | 编辑源代码]

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 所在的位置。conditionbody 是模式变量。它们不是真正的变量。它们在运行时不会有值,而只在模板内部有值。由于点列表表示法,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。如果 ab 不是字面量,那么可以通过生成以下代码来将此模式与 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-carcar 的一个特殊版本,如果它在非对上调用,则不会引发错误。maybe-car 不像假设 match-value 将是一个列表,它使我们能够进行检查,而无需每次都编写 (if (pair? match-value) ...)

如果列表的 car 匹配,则会再次在数据的 cdr 和模式的 cdr(上面的代码片段中的 tl)上使用 match-pattern 宏。这样,tl 可以是另一个模式,也可以是一个变量。

如果 maybe-carmaybe-cdr 遇到不是列表的内容,它们将返回 fail-value,我们将在其中进行检查,如果找到它,则返回它。这样,如果对 maybe-carmaybe-cdr 的任何调用都评估为 fail-value,那么整个 match-pattern 也将评估为 fail-value

宏将使用来自 循环 章节的 exists-in? 来实现字面量。如果在 pattern 中找到 literals 中的标识符,则该标识符将被解释为符号,并且结构中的该位置必须是该标识符,否则将返回 fail-valuefailure-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)))))

这就是宏的功能。

华夏公益教科书