许多Lispers会告诉您,使Lisp与众不同的地方是homoiconicity,这意味着该代码的语法使用与其他数据相同的数据结构表示。例如,这是一个简单的函数(使用Scheme语法),用于计算具有给定边长的直角三角形的斜边:
(define (hypot x y)
(sqrt (+ (square x) (square y))))
现在,谐音说上面的代码实际上可以用Lisp代码表示为数据结构(特别是列表列表)。因此,请考虑以下列表,并查看它们如何“粘合”在一起:
(define #2# #3#)
(hypot x y)
(sqrt #4#)
(+ #5# #6#)
(square x)
(square y)
宏使您可以将源代码视为:清单。每个那些6“子列表”的任一含有指向其他列表,或以符号(在本例中:define
,hypot
,x
,y
,sqrt
,+
,square
)。
那么,我们如何才能使用谐音来“分离”语法并生成宏呢?这是一个简单的例子。让我们重新实现let
我们称为的宏my-let
。提醒一句,
(my-let ((foo 1)
(bar 2))
(+ foo bar))
应该扩展为
((lambda (foo bar)
(+ foo bar))
1 2)
这是使用Scheme“显式重命名”宏†的实现:
(define-syntax my-let
(er-macro-transformer
(lambda (form rename compare)
(define bindings (cadr form))
(define body (cddr form))
`((,(rename 'lambda) ,(map car bindings)
,@body)
,@(map cadr bindings)))))
该form
参数绑定到实际的形式,所以在我们的例子,这将是(my-let ((foo 1) (bar 2)) (+ foo bar))
。因此,让我们看一下示例:
- 首先,我们从表单中检索绑定。
cadr
抓取((foo 1) (bar 2))
表单的一部分。
然后,我们从表单中检索主体。cddr
抓取((+ foo bar))
表单的一部分。(请注意,这是为了在绑定后获取所有子表单;因此,如果表单是
(my-let ((foo 1)
(bar 2))
(debug foo)
(debug bar)
(+ foo bar))
那么身体会是((debug foo) (debug bar) (+ foo bar))
。)
- 现在,我们实际上构建了结果
lambda
表达式,并使用已收集的绑定和主体进行调用。反引号称为“准引用”,表示将准引用内的所有内容都视为文字数据,除了逗号后的位(“ 取消引用”)。
- 定义此宏时有效
(rename 'lambda)
使用lambda
绑定的方法,而不是使用此宏时可能发生的绑定。(这被称为卫生。)lambda
(map car bindings)
返回(foo bar)
:每个绑定中的第一个基准。
(map cadr bindings)
返回(1 2)
:每个绑定中的第二个数据。
,@
执行“拼接”,用于返回列表的表达式:它导致将列表的元素粘贴到结果中,而不是列表本身。
- 总而言之,我们得到列表
(($lambda (foo bar) (+ foo bar)) 1 2)
,$lambda
这里是指已重命名的lambda
。
坦白吧?;-)(如果这对您来说不那么简单,请想象一下为其他语言实现宏系统会有多么困难。)
因此,如果您能够以一种非笨拙的方式“分拆”源代码,则可以拥有用于其他语言的宏系统。有一些尝试。例如,sweet.js针对JavaScript执行此操作。
†对于有经验的Schemer,我有意选择使用显式重命名宏作为defmacro
其他Lisp方言所使用s 之间的中间折衷,syntax-rules
(这将是在Scheme中实现此类宏的标准方法)。我不想写其他Lisp方言,但我不想疏远不熟悉的非Schemers syntax-rules
。
供参考,以下my-let
是使用的宏syntax-rules
:
(define-syntax my-let
(syntax-rules ()
((my-let ((id val) ...)
body ...)
((lambda (id ...)
body ...)
val ...))))
相应的syntax-case
版本看起来非常相似:
(define-syntax my-let
(lambda (stx)
(syntax-case stx ()
((_ ((id val) ...)
body ...)
#'((lambda (id ...)
body ...)
val ...)))))
两者之间的区别在于in 中的所有内容都应用syntax-rules
了隐式#'
,因此您只能在中使用模式/模板对syntax-rules
,因此它是完全声明性的。相反,在中syntax-case
,模式后面的位是实际代码,最后必须返回语法对象(#'(...)
),但也可以包含其他代码。