[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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容