1. 编译器宏
Lisp源代码文本,首先经过读取器,得到了一系列语法对象,
这些语法对象,在宏展开阶段进行变换,最终由编译器/解释器继续处理。
以下我们使用defmacro
定义了一个宏inc
,
(defmacro inc (var)
`(setq ,var (1+ ,var)))
它可以将(inc x)
展开为(setq x (1+ x))
。
inc
宏可以看做对编译器/解释器进行“编程”,它影响了最终被编译/解释的程序。
因此,类似inc
这样的宏,称为编译器宏(compiler macro)。
此外,还有一种宏,称为读取器宏(reader macro),
它在源代码的读取阶段,以自定义的方式,将文本转换为语法对象。
引用(quote)“'
”,就是一个读取器宏,
它将源代码文本'(1 2)
转换成(quote (1 2))
。
2. 用户定义的读取器宏
虽然,引用“'
”是一个读取器宏,但它却不是由用户定义的,
支持用户自定义的读取器宏,是一个很强大的语言特性,
它可以让我们摆脱语法的束缚,创建自己的语言。
2.1 Common Lisp
(1)set-macro-character
在Common Lisp中,我们可以使用set-macro-character
,来模拟引用“'
”的定义,
(set-macro-character #\'
#'(lambda (stream char)
(list (quote quote) (read stream t nil t))))
当读取器遇到'a
的时候,会返回(quote a)
。
其中read
函数可以参考:read。
(2)set-dispatch-macro-character
我们还可以自定义捕获字符(dispatch macro character),
例如,我们定义#?
来捕获后面的文本,
(set-dispatch-macro-character #\# #\?
#'(lambda (stream char1 char2)
(list 'quote
(let ((lst nil))
(dotimes (i (+ (read stream t nil t) 1))
(push i lst))
(nreverse lst)))))
读取器会将#?7
转换成(0 1 2 3 4 5 6 7)
。
(3)get-macro-character
我们还可以自定义分隔符,例如,以下我们定义了#{ ... }
分隔符,
(set-macro-character #\}
(get-macro-character #\)))
(set-dispatch-macro-character #\# #\{
#'(lambda (stream char1 char2)
(let ((accum nil)
(pair (read-delimited-list #\} stream t)))
(do ((i (car pair) (+ i 1)))
((> i (cadr pair))
(list 'quote (nreverse accum)))
(push i accum)))))
读取器会将#{2 7}
转换成(2 3 4 5 6 7)
。
其中,get-macro-character
可以参考:GET-MACRO-CHARACTER。
2.2 Racket
在Racket中,我们可以通过创建自定义的读取器,得到一门新语言,
例如,下面两个文件language.rkt
和main.rkt
,
(1)language.rkt
模块创建了一个读取器,
#lang racket
(require syntax/strip-context)
(provide (rename-out [literal-read read]
[literal-read-syntax read-syntax]))
(define (literal-read in)
(syntax->datum
(literal-read-syntax #f in)))
(define (literal-read-syntax src in)
(with-syntax ([str (port->string in)])
(strip-context
#'(module anything racket
(provide data)
(define data 'str)))))
(2)main.rkt
模块,就可以用新语法进行编写了,
#lang reader "language.rkt"
Hello World!
然后,我们载入main.rkt
,查看该模块导出的data
变量,
> (require (file "~/Test/main.rkt"))
> data
"\nHello World!"
在main.rkt
中,
我们通过#lang reader "language.rkt"
,载入了一个自定义的读取器模块,
该模块必须导出read
,read-syntax
两个函数。
这里,read-syntax
只是简单的获取源代码,导出到data
变量中,
最终返回了一个用于模块定义的语法对象(module ...)
。
在本例中,它把"Hello World!"
转换成了一个模块定义表达式,
(module anything racket
(provide data)
(define data "Hello World!"))
其中,anything
是模块名,racket
是该模块的依赖。
所以,当载入main.rkt
后,我们就可以获取data
的值了。
在实际应用中,我们还可以对源代码进行任意解析,创建自己的语言。
2.3 Emacs Lisp
Emacs Lisp内置的读取器,并不支持自定义的读取器宏,
为了实现读取器宏,我们需要重写Emacs内置的read
函数,
例如,elisp-reader。
Emacs在启动时,会自动载入~/.emacs.d/init.el
文件,然后执行其中的配置脚本,
因此,我们可以在init.el
中调用elisp-reader。
(1)创建~/.emacs.d/init.el
文件,
(add-to-list 'load-path "~/.emacs.d/package/elisp-reader/")
(require 'elisp-reader)
(2)使用git克隆elisp-reader仓库到~/.emacas.d/package
文件夹,
git clone https://github.com/mishoo/elisp-reader.el.git ~/.emacs.d/package/elisp-reader
(3)打开Emacs,自动执行init.el
中的配置,
(4)在Emacs中定义一个读取器宏,然后求值整个Buffer,(M-x ev-b
)
(require 'cl-macs)
(def-reader-syntax ?{
(lambda (in ch)
(let ((list (er-read-list in ?} t)))
`(list ,@(cl-loop for (key val) on list by #'cddr
collect `(cons ,key ,val))))))
(5)测试read
函数的执行结果,(C-x C-e
)
(read "{ :foo 1 :bar \"string\" :baz (+ 2 3) }")
> (list (cons :foo 1) (cons :bar "string") (cons :baz (+ 2 3)))
(car { :foo 1 :bar "string" :baz (+ 2 3) })
> (:foo . 1)
源代码{ :foo 1 :bar "string" :baz (+ 2 3) }
被直接读取成了一个列表对象,
((:foo . 1) (:bar "string") (:baz (+ 2 3)))
对car
函数而言,它看到的是列表对象,并不知道具体的语法是什么。
3. 总结
本文介绍了读取器宏的概念,Lisp各方言中会对读取器宏有不同程度的支持,
我们分析了Common Lisp,Racket以及Emacs Lisp的做法。
读取器宏直接作用到源代码文本上,用户定义的读取器宏可以对读取器进行“编程”,
借此可以支持自由灵活的语法,它是设计和使用DSL的神兵利器。
参考
Common Lisp the Language, 2nd Edition: 8.4 Compiler Macros
ANSI Common Lisp: 14.3 Read-Macros
Let Over Lambda: 4. Read Macros
The Racket Reference: 17.3.2 Using #lang reader
Github: elisp-reader