Common Lispのリーダーマクロで遊んでみた。
mirror: HugeDomains.com - Shop for over 300,000 Premium Domains
リーダーマクロを使うと、特定の指定した文字がソースコードに現れたときに、そこから文字列を読み込んでLispオブジェクト列を生成する関数を実行することができる。
たとえば、Lispのプログラム中に#{ ... }という文字列が現れたら、...の部分はRubyの式として評価する(Rubyのパーサ、評価機は別途用意しなきゃいけないけど)という事が可能になる。
LispのS式というのは、狭義ではいわゆる括弧で構造化された文字列のことを指すが、実際にはLispのオブジェクト列に変換するルールを持った文字列であれば、何でもS式と言ってしまうことができる。なぜなら、Lispのリーダーはリーダーマクロのようにプログラマが変更可能であるから、あるテキストをLispオブジェクト列に変換するリーダーを書けば、そのテキストは立派なS式と言えるのだ。
今回は、次のような変換を行うリーダマクロを書いてみた。
#testcase <テストケース名> <適当な文字列> #end ;; => (<テストケース名> <適当な文字列>)
このリーダマクロを作った理由は、CppUnitの記述の重複をなくすためである。CppUnitという単体テストのツールを使う場合、何も補助ツールがないと、テストスイートとテストケースの名前を3ヶ所くらいに書いて、複数のファイルを行ったりきたりして、と非常に煩雑な作業を強いられる。
しかし、本質的に必要な記述は、テストスイート名、テストケース名、テスト内容だけである。そこで、これをS式で記述して*.cppと*.hppファイルを生成することを考えた。
テストスイートとテストケースの名前は何の問題もないのだけど、肝心のテストケースの中身(C++のコード)は直接Lispの式の中に書くことができない。文字列として書くのはなんだかなぁ(エスケープとか面倒)。そこでリーダーマクロの出番。
Lispに文字列として記述せずに、リーダーマクロを使えば、たとえばEmacsではモードを切り替えてc++-modeの機能を利用することもできてハッピー。
リーダーマクロの最も単純な使い方は、set-dispatch-macro-character を使う方法らしい。set-dispatch-macro-character は引数に2つの文字と、(ストリームと2つの文字を引数とする)関数をとる。
例えば、!! という文字列が現れたときに何かしたいときは、
(set-dispatch-macro-character #\! #\! (lambda (stream char1 char2) ( ... 何かする ...)))
とする。
そんなわけで、今回書いたコードを実行するとこんなことができる
CL-USER> #testcase foo-case stack<hogehoge> hogestack; this->is.test("code"); #end => (FOO-CASE "stack<hogehoge> hogestack; this->is.test(\"code\"); ")
見事目的のC++のコード文字列が手にはいった。あとはこの文字列をテンプレートに流しこんで、cppファイルなどなどを生成してあげればよい。
今回書いたコード。Common Lispは初心者なので効率悪いことやっていそうな気がする。
(set-dispatch-macro-character #\# #\t #'(lambda (stream char1 char2) (declare (ignore char1 char2)) (loop for c = (read-char stream) while (not (eq #\space c))) (let ((testcase-name (read stream)) (terminal-str "") (end-mark "#end")) (if (not (symbolp testcase-name)) (error "Invalid testcase name.") (let* ((raw-ret (with-output-to-string (out) (loop for c = (read-char stream) while (not (string-equal terminal-str end-mark)) do (write-char c out) (setq terminal-str (if (< (length terminal-str) (length end-mark)) (concatenate 'string terminal-str (string c)) (concatenate 'string (subseq terminal-str (- (length terminal-str) (- (length end-mark) 1)) (length terminal-str)) (string c))))))) (ret-str (subseq raw-ret 0 (- (length raw-ret) (length end-mark))))) `(quote (,testcase-name ,ret-str)))))))