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