Qu'ils mangent de la X'ruby(クリスマスリリースがないならクリスマスRubyを作ればいいじゃない)

この記事はRuby Advent Calendar2009の最終日です。昨日は@tomoya55さんでした。



メリークリスマス! = Merry Xmas! ≒ Merry Xml! ということで、XMLRubyの話をします。
TokyuRuby会議*1でこんなことを言った人がいました。
Rubyは90年代のいいとこ取り言語だけど、Scalaは2000年代のいいとこ取り言語だ」
これを聞いた敬虔なRubyistの皆さんはもちろん「2000年代いいとこ取りの結果がXMLかよ!」って思ったと思うんですが

/* http://lampsvn.epfl.ch/svn-repos/scala/scala/trunk/docs/examples/xml/phonebook/phonebook.scala */
package phonebook ;
object phonebook {
  val labPhoneBook = 
    <phonebook>
      <descr>
        This is the <b>phonebook</b> of the 
        <a href="http://acme.org">ACME</a> corporation.
      </descr>
      <entry>
        <name>Burak</name> 
        <phone where="work">  +41 21 693 68 67</phone>
        <phone where="mobile">+41 79 602 23 23</phone>
      </entry>
    </phonebook>;
  // ... snip ...

とはいえXMLリテラルって言う発想は正直なかったし、機能はともかくその素っ頓狂な見た目がやっぱり羨ましかったりするわけです。
そんな素敵リテラルを指を加えて眺めるだけだった日々は昨日でおしまいです。そう、今日はクリスマス。目が覚めると良い子にしていたみんなの枕元のRubyにはXMLリテラルが入っていたのです!!

phone_book = <?xml>
  <phonebook>
    <descr>
      This is the <b>phonebook</b> of the 
      <a href="http://acme.org">ACME</a> corporation.
    </descr>
    <entry>
      <name>Burak</name> 
      <phone where="work">  +41 21 693 68 67</phone>
      <phone where="mobile">+41 79 602 23 23</phone>
    </entry>
  </phonebook>
</xml>

puts phone_book.class.name
#=> REXML::Document

puts phone_book.elements["//phone[@where='mobile']"].text
#=> +41 79 602 23 23

puts phone_book.elements["//descr/a"].attribute('href')
#=> http://acme.org

X'rubyではヒアドキュメントのように*2<?識別子>から</識別子>までの間をREXML::Documentオブジェクトにして返します。無論、開始と終了のところの識別子は任意。まぁxmlにしとくのが無難というかおすすめだけど。
REXMLは使いにくいから嫌だという人はString#xmlを上書きしましょう。Nokogiriを使いたければこんな感じで。

require 'nokogiri'
class String
  def xml
    Nokogiri.parse(self)
  end
end

あとこんな感じに式展開も可能。

message = 'hello'
greeting = <?xml>
  <greeting>
    #{message}
  </greeting>
</xml>

ということで、今年もクリスマスに新しいRubyがリリースされ、伝統と格式は守られました。
http://github.com/technohippy/xruby
それではみなさん、良いお年を。

*1:みなさんそろそろ忘れているかもしれないですが、大事なことなので注記しておきますね。TokyuRubyKaigiとは私がカリスマになったKaigiです

*2:っていうかparse.yのヒアドキュメントのところをあらかたコピペしてでっち上げてるんだけど

クリスマスRubyの作り方

作り方と言うかやったことのメモ。結論から言うとparse.yを"here"で検索してヒアドキュメントのところを片っ端からコピペしてちょっと修正。
まずはparse.yのスキャナ部分をいろいろコピペして、XMLリテラルの開始<?識別子>と終了</識別子>を処理できるように変更。細かいのが分散してて面倒くさいので詳細は省略。ヒアドキュメントと変えたところだけ大まかに書くと

  • "<"のあとに"<"が来たらヒアドキュメントなのに対して、"<"のあとに"?"が来たらXMLリテラルということにした
  • リテラルの終了用のトークンは敢えて識別子の前後に""つけてタグっぽくした

の2点。
これで <?xml> とか書いたときスキャナから tHEREXML_BEG が飛んでくるようになったので次はパーサー。

/* parse.y */
...
/* 1. */
%type <node> singleton strings string string1 xstring xml regexp
...
/* 2. */
primary : literal
        | strings
        | xstring
        | xml
...
/* 3. */
xml     : tHEREXML_BEG string_contents tSTRING_END
/* 4. */
            {
            /*%%%*/
                NODE *node = $2;
                if (!node) node = NEW_STR(STR_NEW0());
                $$ = NEW_CALL(node, rb_intern("xml"), 0);
            /*%
                $$ = dispatch1(xstring_literal, $2);
            %*/
            }
        ;
  1. とりあえずxmlノードを追加しておく
  2. xmlノードは数値や文字列と同じような扱いにする
  3. <?識別子>、文字列、文字列終了、の並びをxmlノードとする
  4. xmlノードは文字列インスタンス(node)のxmlメソッドを呼び出すためのコードを吐くようにする。(NEW_CALLは、第一引数にレシーバ、第二引数にメソッドのID、第三引数にメソッドの引数(引数なしの場合は0)、を受け取ってメソッド呼び出し用のコードを吐くっぽい)

で、最後にStringクラスにインスタンスメソッドxmlを追加すればX'ruby完成。

# prelude.rb
.. snip ..
require 'rexml/document'
class String
  def xml
    REXML::Document.new(self)
  end
end

prelude.rbはなんだかよくわかってないけど、ここに書いておくと最終的にCの文字列になってrubyに組み込まれる模様。もしかしたらもっといい場所があるのかも。
さっそくmakeしてinstallして動作確認。

$ autoconf
$ configure --prefix=/Users/ando/xruby
$ make
$ make install
$ cd /Users/ando/xruby
$ bin/irb
irb(main):001:0> xml = <?xml>
irb(main):002:0* <greeting>hello</greeting>
irb(main):003:0/ </xml>
irb(main):004:0> xml.elements['/greeting'].text
=> "hello"

カンペキ。
いやー、Rubyにオレオレ文法を足すのは思いのほかたのしいすね。



注意C言語は10年以上前に新入社員研修でやったのがほぼ全てで正直よく知りません。そんなんでもコピペで意外となんとかなったよ、という例として御覧下さい。(なんとかなっていないかも・・・)