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)))))))