uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

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は(変なことをするのには)不十分か。

閑話休題

Prolog上でLisp処理系を書くとか昔よくありましたよね。で、BASIC上のProlog処理系上でLispを動かすのが月刊ASCIIの記事になってたりして。今考えると、Prologって偉大なDSL処理系なんですね。Prologでは演算子の定義ができるだけじゃなく、結合性とかも定義できたし。それをいったらAlgolとか、昔の言語ほどDSL機能は強力だったりする。

おまけ

${a} {b} {c} {d}

みたいにクロージャを連ねていくのはGroovy文法的に可能です。
上をリストと見なせば、もっともっとキモイLispが作れます。

*1:私もその一人ではありますが。

*2:AST変換なら可能。

*3:発表でもちょっと述べたようにクロージャ定数については、トップレベルで文法的にブロックと曖昧になるという別の問題もあります。

*4:ちなみにこの人はその場でcallされる。

*5:$じゃ無くてもいいんですが、なんらかのメソッド呼び出しの引数に与えてやる。