LispBuilder-Groovy内部DSL限界突破の試み
昨日、LispBuilderについて、JGGUG主催G*WS第四回にショートプレゼンで発表して参りました。発表資料を公開しておきます。
参加者の方、主催者の皆さん*1、会場を提供くださったCIJのみなさん、ありがとうございました。
他の皆さんの発表は、感想は別に書きますが、いずれも興味深くとてもおもしろかったです。懇親会行けなくて残念でした。
あと、私の発表は濃くて済みませんでした。
ところで、Lisp DSLがなんでこんな変になっちゃったかをちょっと補足しておきます。
ビルダーって言うのはそもそも何かというと、クロージャ評価の際に、そのクロージャ中にコードとして列挙されている文の列を、逐次実行していき、そのときに生じる副作用として木や何らかの構造を作っていく、というものです。副作用というのは、メソッド呼び出しやプロパティの評価の際に生じます。そこにGroovyのMOPなどの機能を使って、構造生成のアクションを結びつけてやるのです。
なので、シンボルの評価や、メソッドの呼び出しには、それにgetPropertyやinvokeMethodをひっかけられるので良いのですが、トップレベルの、つまり式文としての定数、たとえば1とか数値定数や文字列定数などは、評価しても副作用が発生しないので、ビルダーでは扱えないのです。その存在を検知できないのです*2。
なので、$()みたいにメソッドコールで括ってinvokeMethodでひっかけてやったり、$132みたいに整数定数ではなく識別子にしてやって、getPropertyに掛けざるをえないということです。
また、トップレベルの「クロージャリテラル」も、同様に副作用を持たないので同じ問題があります*3。変に聞こえるかもしれませんが、クロージャの中の文は副作用を持つとしても、クロージャリテラルそのもの*4の存在自体はフックできないのです。だから「${ .. }」のようにクロージャに$をつけてメソッドの引数化する必要があります*5。ちなみに、「$({ .. })」とは書かなくていいのは、Groovy文法ルールがそうなってるからです。「$"abc"」もOKで、「$("abc")」とは書かなくても良い。しかし、「$1.0」は不可であり、「$(1.0)」などとせざるをえません。
結果、Lisp DSLをビルダーとして実装する場合、理想的には、以下のように書きたいのですが、
{defun; add1; {a} {plus; a; 1} }
かけなくて悲しいです。以下のようになってしまいました。
${defun; add1; ${a} ${plus; a; $1} }
まあ、$で統一してるっぽくなってるのはせめてもの救いか。
感想
話しはややずれますが、感じたこととしては、DSLの記述能力にとっては、トークナイザー(aka Lexer, lexical analyzer)の柔軟性も重要だなあと。たとえば、C++0xでは、リテラルの接尾子が再定義可能になってますが、こういうこざかしいところが重要。この点、Groovyは(変なことをするのには)不十分か。