内部DSL

RubyDSLDomain 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」のように書いても処理されなくて悲しい。