Emacs之魂(九):读取器宏

  1. 云栖社区>
  2. 博客>
  3. 正文

Emacs之魂(九):读取器宏

何幻 2017-10-31 13:48:03 浏览1724
展开阅读全文

Emacs之魂(一):开篇
Emacs之魂(二):一分钟学会人界用法
Emacs之魂(三):列表,引用和求值策略
Emacs之魂(四):标识符,符号和变量
Emacs之魂(五):变量的“指针”语义
Emacs之魂(六):宏与元编程
Emacs之魂(七):变量捕获与卫生宏
Emacs之魂(八):反引用与嵌套反引用
Emacs之魂(九):读取器宏

    • -

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.rktmain.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",载入了一个自定义的读取器模块,
该模块必须导出readread-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

网友评论

登录后评论
0/500
评论
何幻
+ 关注