gorubyライクな短縮形をClojureで
(このエントリはLisp Advent Calendar 2012 8日目の記事です。)
gorubyとは
gorubyはRubyに標準でついてくる、ゴルフ用のRuby処理系です。ゴルフ用というだけあって、普通のRubyに追加でゴルフに都合のいい機能をいくつかもっています。そのひとつに、「メソッドを短縮形の名前で呼び出せる」機能があります。たとえば、普通のRubyで以下のように書くコードは、
['foo','bar','baz'].each_with_index do |c, i| puts "#{i}: #{c}" end
gorubyでは以下のように書けます。
['foo','bar','baz'].ew do |c, i| ps "#{i}: #{c}" end
元のコードの"each_with_index"がgoruby版では"ew"に、"puts"が"ps"になっています。
gorubyでは、オブジェクトに定義されていないメソッドの呼び出しが見つかると、それを定義されたメソッドの短縮形として解決しようとします。実装としては、未定義のメソッドが呼ばれた時点でmethod_missingが呼び出され、その中でオブジェクトに定義された全メソッドを取得して名前の短い順にソートし、正規表現("ew"なら/^.*e.*w.*$/)にマッチする最初のメソッドを呼び出す、ということをやっています。
この「メソッドを短縮形で呼べる」というのはおもしろい機能だと思ったのでClojureでやってみました。ただし、短縮形の解決を実行時にするのは効率が悪そうなのでマクロで頑張ることにします。というわけで、作ったマクロが goclojure です。
goclojureマクロ
goclojureマクロは、
(goclojure <expr> ...)
という形で使うと、
(defn fibs [n] (letfn [(rec [a b] (lazy-seq (cons a (rec b (+ a b)))))] (take n (rec 0 1))))
goclojureを使うと以下のように書き直せます。
(goclojure (dn fibs [n] (lf [(rec [a b] (zq (cn a (rec b (+ a b)))))] (tk n (rec 0 1)))))
これを見ても分かるように、goclojureでは関数だけでなく、マクロ(や特殊形式)に対しても短縮形が使えます。ifがiに、fnがfに、letがlに短縮できて、ゴルフ的には非常に嬉しそうです :-)
ちなみに、Clojureの標準の名前空間であるclojure.coreには588の名前が提供されていますが、goclojureを使ってできる限り短い名前に短縮すると、4つの名前が4文字に短縮でき、残りの584の名前が3文字までに短縮できるようです。
上の図は、各文字数の名前がいくつあるかを表したグラフです。赤の系列が元のclojure.coreの分布、青の系列がgoclojureを使って短縮したときの分布です。名前の長さの平均は、元が約8.3文字に対し、goclojureを使うと約2.4文字になるようです。驚異的な圧縮率です(笑)
実装について
goclojureでは、マクロ展開時に短縮形の展開器を呼び出し、本体に含まれるフォームをトラバースして短縮形を展開します。短縮形の展開器は、その時点のスコープで見えている変数をすべて含む環境をとり、識別子に出会うたびにその識別子が何かの名前の短縮になっていないか環境の中を(gorubyと同様の方法により)探索します。マクロが存在すると、一般的にはどれが展開すべき識別子か判別することができないので、トラバースしていく過程でマクロ呼び出しのフォームに出会ったらmacroexpandを使って先に展開します。
ところで、goclojureはローカル変数の短縮に対応していません。これは、マクロの存在による制限です。
マクロはgensymなどによって暗黙にローカル変数を導入することがあります。短縮形の展開器にとっては、あるローカル変数がプログラマによって導入されたものなのか、マクロによって導入されたものかは分かりません。そのため、もしローカル変数の短縮を許してしまうと、短縮形の展開によって、マクロが暗黙に導入したローカル変数を意図せずに参照してしまう可能性があります。これを排するため、ローカル変数は短縮できないという制限を設けました。
この制限により、ローカル変数は短縮形の展開後の名前を探す環境には含めなくてよいものの、ローカル変数の参照を何かの名前の短縮形だと思って誤って展開されてしまっては困るので、何がローカル変数であるかは別に環境を用意して管理しておく必要があります。そのため、短縮形の展開器はトラバースの過程で束縛を含む特殊形式(fn*/let*/letfn*/loop*/catch)に出会ったら、このローカル変数を管理する環境に追加していきます。
goclojureのソースコードはこちら→athos/goclojure · GitHub。
おわりに
実は、goclojureでやっていることは、以前作ったsyntactic-closureとほとんど同じなんですが、見せ方だけでずいぶん違ったものになるのはおもしろいですね。
ところで、12/8にNGK2012BのLTで、事前にgoclojureについて発表したところ、
#NGK2012B goclojureという名前が長い
「(ゴルフで使うには)goclojureという名前が長い」と言われてしまったので、名前はそのうち変えるかもしれません :-P