美文网首页Lispemacs lisp语言介绍
[Emacs] Emacs之魂(六):宏与元编程

[Emacs] Emacs之魂(六):宏与元编程

作者: 何幻 | 来源:发表于2017-06-21 21:39 被阅读510次

    数据和代码

    如果说Lisp语言有一个特性最能使人津津乐道的话,我想应该是它的宏系统(macro system)了吧,
    在Lisp语言中,程序和代码的表现形式(textual representation)几乎一致,造就了它无与伦比的元编程能力。
    这种对称性,使得Lisp语言可以像处理数据一样优雅的处理代码本身。

    并且和其他语言不同的是,Lisp的宏系统,并不是简单的文本操作,
    而是建立在语法对象(syntax object)基础之上。

    前文提到过,我们直接写(foo bar bar)表示函数调用或者宏调用(macro call),
    加引用'(foo bar bar)表示列表字面量,
    直接写x表示变量或者函数,加引用'x表示符号(symbol)。

    如果我们把列表字面量和符号看做数据,把变量和函数调用看做程序,
    那么数据和程序的表现形式(textual representation)几乎是相同的,只差一个引用。
    所以,如果一个函数能够处理数据(列表/变量),那么它也一定能够处理被引用的程序,
    同理,如果一个函数能够返回一段数据(列表/变量),那么去掉引用之后(使用eval),
    也可以看做它是返回了一段程序。

    例如,

    (defun inc (var)
        (list 'setq var (list '1+ var)))
    
    (inc 'x)    ; (setq x (1+ x))
    

    我们定义了一个inc函数,它接受var作为参数,返回了一个列表。
    即,(inc 'x)的求值结果为(setq x (1+ x))
    其中,(setq x (1+ x))是一个列表。

    我们可以通过eval直接把返回的列表当做程序来执行,

    (defvar x 0)
    (eval (inc 'x))
    
    x    ; 1
    

    x的值被修改了,变成了1

    定义一个宏

    我们只需要将上文的inc稍作修改,就可以把它转换成一个宏(macro),
    我们只需要将defun改成defmacro即可,

    (defmacro inc (var)
        (list 'setq var (list '1+ var)))
    

    现在inc就是一个宏(macro)了,它的使用方式和函数非常相似,

    (defvar x 0)
    (inc x)
    
    x    ; 1
    

    我们看到,这里直接使用了(inc x),而不是(inc 'x)
    并且,(inc x)的作用和直接写程序(setq x (1+ x))是一样的。

    (inc x)我们称之为宏调用(macro call),
    (setq x (1+ x))我们称之为宏展开(macro expansion)后的程序。

    编译器或者解释器会采用不同的策略进行宏展开,
    一般而言,编译器会在求值程序之前,将代码中所有的宏(macro)进行展开,
    即,将所有的宏调用(inc x),替换成它返回的那段程序(setq x (1+ x))
    直到代码中不再包含宏(macro)为止,然后再进行编译。

    一个简单的解释器实现,可能会一边执行程序一边进行宏展开操作,
    它会在运行时,通过判断符号(symbol)的类型,来决定进行函数调用还是宏调用。
    这样可能会有助于理解宏的递归展开问题。

    一个宏展开式中,可能还会包含其它的宏,也可能还会包含另一个宏的定义。
    (以后的文章中,我们会介绍)

    因此,在宏定义中,进行的具有副作用(side effect)的操作,
    其执行时机并不是在运行时,而是在宏展开阶段,
    而如果宏实参中包含了带有副作用的操作,那么它可能被展开到源代码中的多个位置,
    从而被执行多次。

    语法对象

    在Emacs Lisp中,宏变量inc实际上是一个转换函数,
    它将var转换成了(list 'setq var (list '1+ var)),即把符号(symbol)转换成了一个列表对象。

    宏变量的值与函数一样会保存在符号(symbol)inc的function cell中,
    因此,一个符号(symbol)不可能既表示一个函数又表示一个宏(macro)。

    当Lisp解释器遇到一个符号(symbol)的时候,
    会判断它到底是一个变量,一个函数还是一个宏(macro)。

    (defun add1 (x)
        (+ x 1))
    
    (defvar a 1)
    (add1 a)
    

    如果是一个函数,且当前进行的是函数调用(add1 a)
    那么就会先求值它的实参,a求值为1
    再将add1的形参x绑定为实参的值1,再求值函数体,
    即,求值(+ x 1),结果为2

    (defmacro inc (var)
        (list 'setq var (list '1+ var)))
    
    (defvar x 0)
    (inc x)
    x    ; 1
    

    如果是一个宏(macro),且当前进行的是宏调用(inc x)
    那么它并不会像函数那样先求值函数体,而是直接将宏形参绑定为宏调用的实参值。
    即,var绑定为符号(symbol)x

    值得注意的是,宏调用的实参,是一个符号(symbol),它是一个Lisp对象,而不是一个字符串,
    宏(macro)所返回的结果,也是一个Lisp对象。

    更明确的说,宏(macro)是一个针对语法对象(syntax object)的变换函数,
    它对读取器获得的语法对象(syntax object)进行变换。
    在某些Lisp方言,例如Scheme,这些语法对象(syntax object)包含了上下文信息,使用它们可以编写出强大而灵活的宏(macro)。

    这里容易引起混乱的是,在Emacs Lisp中,直接使用了符号和列表表示了语法对象,
    而实际上语法对象是一个数据结构,在其内部包含了符号和列表的信息。
    这样做的好处是,在宏展开阶段宏(macro)接受和返回的都是语法对象,
    而在运行时阶段,处理的都是运行时对象了。
    (例如:syntax->datum和datum->syntax

    通过以下程序我们可以验证,var确实是一个符号(symbol)。

    (defmacro inc (var)
        (message "%s" (symbolp var))    ; t
        (list 'setq var (list '1+ var)))
    

    我们之前十分小心的区分了标识符,符号(symbol)和变量,
    是为了在类似这样的场景中保持清醒。

    标识符经过Lisp读取器,在Lisp内部会变成一个符号(symbol),它是一个语法对象,
    然后Lisp会对所有的宏(macro)进行展开,将这些语法对象绑定到宏形参上,对语法对象进行变换。
    最后,求值器在运行时会求值这些符号(symbol),得到一个变量值或者函数值。

    因此,编写宏(macro)可以看作是对编译器或者解释器进行编程,
    Lisp允许用户在表达式被求值之前对它进行一些变换。

    总结

    本文初步介绍了Lisp的宏系统,展示了宏调用与函数调用之间的异同,
    我们发现Lisp的宏系统是建立在语法对象(syntax object)基础之上的,而不是简单的进行文本替换。
    此外,由于Emacs Lisp的宏(macro)不是卫生的(hygienic),所以会和Common Lisp一样出现变量捕获问题。
    下文我们开始介绍一些Lisp宏的常见陷阱和用法。

    参考

    GNU Emacs Lisp Reference Manual
    Chez Scheme Version 8 User's Guide
    An Introduction to Scheme and its Implementation

    相关文章

      网友评论

        本文标题:[Emacs] Emacs之魂(六):宏与元编程

        本文链接:https://www.haomeiwen.com/subject/ykztcxtx.html