Emacs Lisp にリーダーマクロを実装してみた
今回は、前々から構想を練っていた Emacs Lisp 上でリーダーマクロを実現する話をします。
リーダーマクロって?
Lisp インタプリタが S 式を read するときに自由に展開することができるマクロの一種で、 Lisp の構文を独自に拡張したりすることができます。
実際に Lisp インタプリタにリーダーマクロを登録するには set-macro-character を、第一引数にマクロ展開を実行したい文字、第二引数にマクロ展開を実行する関数を指定して呼び出します。第二引数で指定された関数内で stream から文字を読み込んだりして S 式を構築し、多値として返すことでマクロを展開したという意味になります。例えば $ という文字 [1] を nil に展開する場合は以下のように書きます。
[1] | φっぽい |
(set-macro-character #\$ (lambda (stream char) nil)) (setq foo $) ; => (setq foo nil)
詳しい説明は他所でなされているので本題に移ります。
Emacs Lisp でリーダーマクロ
Emacs Lisp は残念ながらリーダーマクロに対応していません。ただし、ソースコードに少し手を入れることでそれっぽいことはできるようになります。
準備
Emacs 22.1 のソースコードに対して本エントリに添付されているパッチを適用してコンパイルしてください。
% cd ~/tmp % wget http://ftp.gnu.org/pub/gnu/emacs/emacs-22.1.tar.gz % wget http://dev.ariel-networks.com/Members/matsuyama/stuff/emacs-22-1-reader-macro-patch/download -O emacs-22.1-reader-macro.patch % tar xf emacs-22.1.tar.gz % patch -p0 < emacs-22.1-reader-macro.patch % cd emacs-22.1 % ./configure --with-x --with-gtk % make
動作を確認には以下のコマンドを実行するとよいでしょう。
% src/emacs -q&
追加された関数
cl-read-char
cl-read-char &optional stream eof-error-p eof-value recursive-p => char
Common Lisp の read-char と同じですが、 Emacs Lisp 標準の関数と名前が被るので、仕方なく cl- というプリフィックスが付いています [2] 。
[2] | マクロ展開時だけ read-char の関数セルをいれかえるとか考えたけど面倒 |
peek-char
peek-char &optional stream eof-error-p eof-value recursive-p => char
Common Lisp の peek-char と同じです。
read-delimited-list
read-delimited-list char &optional stream recursive-p => list
Common Lisp の read-delimited-list と同じです。
set-macro-character
set-macro-character char function &optional non-terminating-p readtable => nil
Common Lisp の set-macro-character とほぼ同じですが、 function の意味が若干異なります。 Emacs Lisp は多値を返せないので、かわりに function はリストを返します。先ほどの $ を nil に展開するリーダーマクロは、 Emacs Lisp では以下のように書きます。
(set-macro-character ?$ (lambda (stream char) (list nil)))
function が nil を返した場合は展開結果が無視されます。
; 独自のコメント構文 (set-macro-character ?! (lambda (stream char) (while (not (eq (cl-read-char stream nil ?\n) ?\n))) nil)) ! ここはコメントとして解釈される (message "Hello")
また、 function が t を返した場合は通常の展開処理が実行されます。
; C++ 風のコメント構文 (set-macro-character ?/ (lambda (stream char) (if (eq (peek-char stream) ?/) (prog1 nil (while (not (eq (cl-read-char stream nil ?\n) ?\n)))) t)) t) // ここはコメントとして解釈される (/ 4 2) // => 2
get-macro-character
get-macro-character char &optional readtable => list
Common Lisp の get-macro-character と同じです。
Perl みたいに連想リストにアクセスする
連想配列にアクセスするには assoc や assq を使いますが、 Perl っぽく {} でアクセスしたい!
(defvar _keys nil) (defmacro _assoc (a) `(cdr (assoc ,(pop _keys) ,a))) (set-macro-character ?{ (lambda (stream char) (push (read stream) _keys) nil)) (set-macro-character ?} (lambda (stream char) (list (list '_assoc (read stream))))) (setq foo '((:foo . (:bar . 100)))) {:foo}{:bar}foo ; => 100
連想リストは書くのが大変なので JSON で書く
(defvar read-in-json nil) (set-macro-character ?: (lambda (stream char) (not read-in-json)) t) (set-macro-character ?, (lambda (stream char) (not read-in-json))) (set-macro-character ?{ (lambda (stream char) (let (hash key value (read-in-json t)) (while (not (eq key '})) (setq key (read stream)) (when (not (eq key '})) (setq value (read stream)) (push (cons key value) hash))) (list (nreverse hash))))) (set-macro-character ?} (lambda (stream char) (list (intern "}")))) (pp '{ "foo": { "bar": 100 }, "hoge": [1, 2, 3, 5] }) ; => ; (("foo" ; ("bar" . 100)) ; ("hoge" . ; [1 2 3 5]))
- Category(s)
- emacs
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/matsuyama/implement-emacs-lisp-reader-macro/tbping