木構造データの妥当性をinstace_evalの黒魔術で検証してみる

タイトルはおおげさすぎ。きっかけは

新しいwhyプロダクト、ShoesはGUIツールキット。 サンプルはこんな感じ。

Shoes.app do
  button "Press Me" do
    alert "You pressed me"
  end
end

こういうAPIは好みだ。

http://www.rubyist.net/~matz/20070801.html#p02

を見たこと。
「あーinstance_evalはやっぱり便利ですよね」と思ってみてたんだけど*1、この構造って再帰的に使えるよなぁ、と思ったのでコードでメモ。
ポイントは、上のソース中のbuttonというメソッド。buttonメソッドはソースの字面をみると定義なしに突然使われているんだけど、instance_evalの黒魔術を使うとこういうことができる。obj.instance_eval do .. end という構文を使うとブロックの内容がobjのコンテキストで実行されるので、objの中にbuttonメソッドが定義されてさえいればブロックの中で突然にbuttonメソッドが使える、というわけ。
裏をかえせば、objのコンテキストで定義されてないメソッドがブロックの中で使われた時に、NoMethodErrorで落とすことができるわけで、こいつを利用してやれば、「ある構造はある構造の子供にはならない」という制約を満たす木構造データを生成できる。例えば、htmlタグの下にはいきなりpタグはこない、とか、そいういうの。

以下ソース。

module ClassBuilder
  module_function

  def create_classes(mod,dtd)
    klass_name_list = ((dtd.values.flatten + dtd.keys).uniq)
    klass_name_list.each {|name| define_class(mod,name,dtd[name])}
    klass_name_list.each do |name|
      klass = mod.const_get(class_name(name))
      klass_name_list.each do |name2|
        klass.const_set(class_name(name2),mod.const_get(class_name(name2)))
      end
    end
  end
  
  def define_class(mod,name,children)
    mod.module_eval(<<EOS0)
class #{class_name(name)}
  def initialize(*data,&block)
    if block_given?
      @_children = []
      self.instance_eval(&block)
    end
    @_data = data
  end
end
EOS0
    return unless children

    children.each do |method_name|
      mod.module_eval(<<EOS1)
class #{class_name(name)}
  def #{method_name.to_s}(*data,&block)
    @_children << #{class_name(method_name)}.new(*data,&block)
  end
end
EOS1
    end
  end

  def class_name(name) name.to_s.capitalize end
end

##### Main

undef p
module HTML;end

ClassBuilder.create_classes(HTML,{
  :html => [:head,:body],
  :head=>[:title],
  :body=>[:h1,:p]
})

require 'pp'
pp HTML::Html.new {
  head {
    title "test html"
    # p "hoge"
  }
  body {
    # body {h1 "foo"}
    h1 "title"
    p "paragraph"
  }
}

HTMLの構造のごくごく一部分だけとりだしたデモ。構造を表現するクラスを生成するのがMain以下のcreate_classes。第一引数として渡したモジュールの中に、第二引数として渡した親子関係の制約を保つようなクラスが自動的に生成される。制約はお手軽に、親となるシンボルをキーにして、子供要素になりうるシンボルの配列を値にしたHashで与える。
あとは、ルートとなるクラスをnewしてブロックにその中身を書く。子供になりうる要素に対応するメソッドが自動的に定義されていて、これも同様にブロックを受けとるので、あとは再帰的に記述するだけ。ブロックをあたえなければ勝手にリーフになる。実行すると、こんなん。

#<HTML::Html:0x1002ad2c
 @_children=
  [#<HTML::Head:0x7ff9cd2c
    @_children=[#<HTML::Title:0x7ff9cc78 @_data=["test html"]>],
    @_data=[]>,
   #<HTML::Body:0x7ff9cc3c
    @_children=
     [#<HTML::H1:0x7ff9cb88 @_data=["title"]>,
      #<HTML::P:0x7ff9cb38 @_data=["paragraph"]>],
    @_data=[]>],
 @_data=[]>

まーあとは、この構造をなめてHTML文字列を生成するなりなんなりするんだろうな。面倒なので略。
コメントを外して、bodyの中にbodyをいれたり、headの中にpを入れたりしようとすると、その構造は与えた制約を満たさないのでNoMethodErrorがとんできてちゃんと落ちる。つまり、こういう構造がちゃんと返ってきた時点で最初に与えた親子間の制約がちゃんと満たされていることが保証される。といっても、制約としてはとっても貧弱で「子供要素として認められない要素は子供要素になっていない」ってのが書けるだけ。この要素は必須とか、これは一回だけ登場、とかは書けない。まあでも、そのへんの拡張は制約の記法さえ決めちゃえば考えることはないですよね、ということで略。

あー。ひととおり書いてみたけど、「何を当然なことをちまちま書いてるんだろう」という気になってきた。DSL流行りだしこれくらいみんな使ってるか。いちいち記事として書く意味はなかったかもしれないな。ま、いいや自分用のメモだし。

*1:Shoesが本当にinstance_eval使ってるかどうかは知らない