1. 編譯器宏
Lisp源代碼文本竭鞍,首先經(jīng)過讀取器富雅,得到了一系列語法對(duì)象,
這些語法對(duì)象臭埋,在宏展開階段進(jìn)行變換弱睦,最終由編譯器/解釋器繼續(xù)處理百姓。
以下我們使用defmacro
定義了一個(gè)宏inc
,
(defmacro inc (var)
`(setq ,var (1+ ,var)))
它可以將(inc x)
展開為(setq x (1+ x))
况木。
inc
宏可以看做對(duì)編譯器/解釋器進(jìn)行“編程”垒拢,它影響了最終被編譯/解釋的程序旬迹。
因此,類似inc
這樣的宏求类,稱為編譯器宏(compiler macro)奔垦。
此外,還有一種宏尸疆,稱為讀取器宏(reader macro)椿猎,
它在源代碼的讀取階段,以自定義的方式寿弱,將文本轉(zhuǎn)換為語法對(duì)象犯眠。
引用(quote)“'
”,就是一個(gè)讀取器宏症革,
它將源代碼文本'(1 2)
轉(zhuǎn)換成(quote (1 2))
筐咧。
2. 用戶定義的讀取器宏
雖然,引用“'
”是一個(gè)讀取器宏地沮,但它卻不是由用戶定義的嗜浮,
支持用戶自定義的讀取器宏羡亩,是一個(gè)很強(qiáng)大的語言特性摩疑,
它可以讓我們擺脫語法的束縛,創(chuàng)建自己的語言畏铆。
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))))
當(dāng)讀取器遇到'a
的時(shí)候楷怒,會(huì)返回(quote a)
。
其中read
函數(shù)可以參考: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)))))
讀取器會(huì)將#?7
轉(zhuǎn)換成(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)))))
讀取器會(huì)將#{2 7}
轉(zhuǎn)換成(2 3 4 5 6 7)
。
其中撮胧,get-macro-character
可以參考:GET-MACRO-CHARACTER桨踪。
2.2 Racket
在Racket中,我們可以通過創(chuàng)建自定義的讀取器芹啥,得到一門新語言锻离,
例如铺峭,下面兩個(gè)文件language.rkt
和main.rkt
,
(1)language.rkt
模塊創(chuàng)建了一個(gè)讀取器汽纠,
#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
模塊逛薇,就可以用新語法進(jìn)行編寫了,
#lang reader "language.rkt"
Hello World!
然后疏虫,我們載入main.rkt
永罚,查看該模塊導(dǎo)出的data
變量,
> (require (file "~/Test/main.rkt"))
> data
"\nHello World!"
在main.rkt
中卧秘,
我們通過#lang reader "language.rkt"
呢袱,載入了一個(gè)自定義的讀取器模塊,
該模塊必須導(dǎo)出read
翅敌,read-syntax
兩個(gè)函數(shù)羞福。
這里,read-syntax
只是簡單的獲取源代碼蚯涮,導(dǎo)出到data
變量中治专,
最終返回了一個(gè)用于模塊定義的語法對(duì)象(module ...)
。
在本例中遭顶,它把"Hello World!"
轉(zhuǎn)換成了一個(gè)模塊定義表達(dá)式张峰,
(module anything racket
(provide data)
(define data "Hello World!"))
其中,anything
是模塊名棒旗,racket
是該模塊的依賴喘批。
所以,當(dāng)載入main.rkt
后铣揉,我們就可以獲取data
的值了饶深。
在實(shí)際應(yīng)用中,我們還可以對(duì)源代碼進(jìn)行任意解析逛拱,創(chuàng)建自己的語言敌厘。
2.3 Emacs Lisp
Emacs Lisp內(nèi)置的讀取器,并不支持自定義的讀取器宏朽合,
為了實(shí)現(xiàn)讀取器宏俱两,我們需要重寫Emacs內(nèi)置的read
函數(shù),
例如旁舰,elisp-reader锋华。
Emacs在啟動(dòng)時(shí),會(huì)自動(dòng)載入~/.emacs.d/init.el
文件箭窜,然后執(zhí)行其中的配置腳本毯焕,
因此,我們可以在init.el
中調(diào)用elisp-reader。
(1)創(chuàng)建~/.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,自動(dòng)執(zhí)行init.el
中的配置芜辕,
(4)在Emacs中定義一個(gè)讀取器宏尚骄,然后求值整個(gè)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)測(cè)試read
函數(shù)的執(zhí)行結(jié)果侵续,(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) }
被直接讀取成了一個(gè)列表對(duì)象倔丈,
((:foo . 1) (:bar "string") (:baz (+ 2 3)))
對(duì)car
函數(shù)而言,它看到的是列表對(duì)象状蜗,并不知道具體的語法是什么需五。
3. 總結(jié)
本文介紹了讀取器宏的概念,Lisp各方言中會(huì)對(duì)讀取器宏有不同程度的支持轧坎,
我們分析了Common Lisp宏邮,Racket以及Emacs Lisp的做法。
讀取器宏直接作用到源代碼文本上缸血,用戶定義的讀取器宏可以對(duì)讀取器進(jìn)行“編程”蜜氨,
借此可以支持自由靈活的語法,它是設(shè)計(jì)和使用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