内部DSL
RubyはDSL(Domain Specific Language)を定義するプログラミング言語として期待されているという。この方面の権威らしいMartin Fowlerによれば、DSLには長い歴史がある。SQLとかCSSとかMakefileとか、ともかく何らかの目的に特化した記述言語をDSLと呼ぶ。かつて、こうしたDSLを設計・実装するには、yaccやらlexを使ってパーザーを作ったり、なんちゃってパーザーを各種プログラミング言語で作るのがUnix的常套手段だった。パーザージェネレーターは、慣れればそれほど使うのは難しくないものの、広く使われていたとは言えない。
その都度ごとにパーザーを作るようなDSLを外部DSL(External DSL)と呼び、これと対置する形でMartin Fowlerは内部DSL(Inner DSL)を定義する。内部DSLとは、既存のプログラミング言語自体を流用してDSLを作る方法。Rubyの文法は、いくつかの点で内部DSLに向いているという。もちろん劇的な成功例はRails。
HTMLのtableタグを吐き出すスクリプトを、内部DSLっぽく作ってみた。実は仕事で結構困っていて、いつか何らかのスクリプトを書こうと思っていた。単純にCSVファイルをタグに変換するだけで十分だと思っていたけど、拾い読みしている「Design Patterns in Ruby」(Russ Olsen)にDSLの説明があって、ちょうどやってみたくなったところだった。
# border 1 cellspacing 0 head "Language",2008,2009,:bg => "ccf" table <<HERE Ruby,111,333 Python,200,400 Perl,50,300 HERE
というテーブル用データを食わせると、HTMLの表を吐き出す。スクリプト本体は、
#!/usr/bin/ruby $border = 1 $cellspacing = 1 $heading_bg = {:bg => "fff"} def border(val = 1) $border = val end def cellspacing(val = 1) $cellspacing = val end def head(*headings) $headings = *headings if Hash === $headings.last $heading_bg = $headings.pop end end def table(body) $table = body.split(/\n/) end def make_table put_header put_headings if $headings $table.each do |tr| print "<tr>\n" tr.split(/,/).each do |td| print " <td>#{td}</td>\n" end print "</tr>\n" end put_footer end def put_headings print "<tr style=\"background:\##{$heading_bg[:bg]}\">\n" $headings.each do |th| print " <th>#{th}</th>\n" end print "</tr>\n" end def put_header print "<table border=\"#{$border}\" cellspacing=\"#{$cellspacing}\">\n" end def put_footer print "</table>\n" end eval(File.read(ARGV.shift)) make_table
と書いた。結果は、
$ ./table data.txt <table border="1" cellspacing="0"> <tr style="background:#ccf"> <th>Language</th> <th>2008</th> <th>2009</th> </tr> <tr> <td>Ruby</td> <td>111</td> <td>333</td> </tr> <tr> <td>Python</td> <td>200</td> <td>400</td> </tr> <tr> <td>Perl</td> <td>50</td> <td>300</td> </tr> </table>
のようになる。
ポイントは、元のデータファイルが、実はRubyスクリプトそのもので、これを処理スクリプトから読み出してevalしてるだけというところ。DSLっぽくするためにトップレベルの関数的メソッドを使っている。
内部DSLのメリットは、ベースとしているプログラミング言語のパーザーを使えること。コメントアウトとか、文字のエスケープとか、面倒なことを考えなくていい。うっかりやってしまってから驚いたけど、Rubyぐらいいい加減に処理してくれると、「2008,2009」が数値か文字列かということすら意識する必要もない。
一方、内部DSLデメリットは、純粋なDSL利用者に無用な記法を強いること。Rubyのハッシュリテラルやシンボルの記法とか、上の例だとヒアドキュメントとか、ちょっと気持ち悪い。例えば、
head "Language",2008,2009,:bg => "ccf"
というのは、
head "Language",2008,2009 headbg "ccf"
という風にしたほうがいいのかもしれない。
このDSLの潜在ユーザーであるぼくの同僚は、表作成のための元データを書いているつもりでいて、実際には、そうとは知らずにRubyスクリプトそのものを書くことになる。
内部DSLのメリットとして、ベースとなる言語(Ruby)が備える機能がそのまま流用できるということも大きそうだ。ちょっとした計算や繰り返し処理をやるなんてことも簡単にできる……、のだけど、この例だと肝心のtable本体を文字列一発で突っ込んでしまっていて、「111*2」のように書いても処理されなくて悲しい。