[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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容