[Emacs] Emacs之魂(九):讀取器宏

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.rktmain.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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末飒炎,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子族扰,更是在濱河造成了極大的恐慌厌丑,老刑警劉巖定欧,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件渔呵,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡砍鸠,警方通過查閱死者的電腦和手機(jī)扩氢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來爷辱,“玉大人录豺,你說我怎么就攤上這事》构” “怎么了双饥?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長弟断。 經(jīng)常有香客問我咏花,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任昏翰,我火速辦了婚禮苍匆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棚菊。我一直安慰自己浸踩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布统求。 她就那樣靜靜地躺著检碗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪码邻。 梳的紋絲不亂的頭發(fā)上后裸,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音冒滩,去河邊找鬼微驶。 笑死,一個(gè)胖子當(dāng)著我的面吹牛开睡,可吹牛的內(nèi)容都是我干的因苹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼篇恒,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼扶檐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胁艰,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤款筑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后腾么,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體奈梳,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年解虱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了攘须。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡殴泰,死狀恐怖于宙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情悍汛,我是刑警寧澤捞魁,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站离咐,受9級(jí)特大地震影響谱俭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一旺上、第九天 我趴在偏房一處隱蔽的房頂上張望瓶蚂。 院中可真熱鬧,春花似錦宣吱、人聲如沸窃这。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杭攻。三九已至,卻和暖如春疤坝,著一層夾襖步出監(jiān)牢的瞬間兆解,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國打工跑揉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锅睛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓历谍,卻偏偏與公主長得像现拒,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子望侈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容