Amrita2はどのように動くのか

2.0.1をリリースして、同時にいくつかドキュメントを追加しました。

この中で、HowAmrita2Worksというのを翻訳(プラス英語でうまく言えなかったニュアンスを追加)します*1

内部構造に関する説明なのですが、中身を知りたい人だけではなくて、普通にユーザとして使う方にも、これが一番わかりやすいのではないかと思います。

概略

Amrita2は、以下の3つのパートで構成されています。

つまり、次のように動作します。

テンプレート→(プリプロセッサ)→ピュアXMLのテンプレート→(Hpricotパーサ+Amrita2コンパイラ)→Rubyコード→モデルデータを与えて実行→結果のHTML。

コンパイラ

Amrita2の基本的なアイディアは、特定の属性が設定されたHTMLエレメントをRubyのコードに変換して実行するということです。

たとえば

<span am:src="xxx" />

というテンプレートを下のようなコードに変換するとか

def render_with(x)
   "<span>" + x[:xxx] + "</span>"
end

あるいは

<p am:src="body">
   <span am:src="template" /> is a template library for <span am:src="lang" />
</p>

というテンプレートを下のようなコードに変換し

def render_with(x)
  $_ = x[:body]
  __stream__ = "<p>"
  __stream__ << $_[:template]
  __stream__ << " is a template library for "
  __stream__ << $_[:lang]
  __stream__
end

次のように実行するということです。

template.render_with :body=>{ :template=>'Amrita2', :lang => 'Ruby' }

そして、次のような結果を出力するわけです。

<p>Amrita2 is a template library for Ruby</p>

hello.rbというサンプルの、set_trace()という行を生かせば、実際に内部でどのようなRubyコードを生成するか確認することができます。

実装の都合とかいろいろな理由で実際のRubyコードはずっと複雑ですが、テンプレートを上記のようなRuby コードにするというのが、Amrita2の基本的なアイディアです。

プリプロセッサ

テンプレートの動的な部分を、属性で指定するという方法は、テンプレートが整形式のXMLになるというメリットがありますが、実際にやってみると、属性をたくさん指定するというのは結構見づらかったので、同じ意味のことをもっと簡単に書けるAmXMLという記述方法をサポートしました。

AmXMLの記述は、全て、プリプロセッサによっていったんXMLに変換されてから処理されます。

たとえば、次のようなテンプレートは

<<html<
  <<body<
    <<h1 :title>>
    <<p :body<
      <<:template>> is a html template libraly for <<:lang>>

こうなります。

<html>
  <body>
    <h1 am:src="title" />
       <p am:src="body">
          <span am:src="template" /> is a template library for <span am:src="lang" />
       </p>
    </h1>
  </body>
</html>

XMLに慣れていたりツールが揃っている場合には、最初からこの記述方式で書くこともできます。

詳細はhttp://retro.brain-tokyo.net/projects/amrita2/wiki/AmXML:AmXMLを参照してください。

フィルター

Amrita2(のXML to Ruby コンパイラ)は、フィルターによって、どのように Ruby に変換するかを、コントロールすることができます。

<<a :link | Attr[:href=>:url, :body=>:text]>>

これを同等なXMLで記述するとこうなります。

<a am:src="link" am:filter="Attr[:href=>:url, :body=>:text]" />

am:filter属性は、Rubyのソースとして評価され、Filterオブジェクトを生成します。このFilterオブジェクトが、コンパイルを制御します。

この例では、Attrというフィルターが、次のコードと同等の Rubyコードを生成します。

def render_with(x)
  $_ = x[:link]
  __stream__ << "<h1 href='"
  __stream__ << $_[:url]
  __stream__ << "' >"
  __stream__ << $_[:text]
  __stream__ << "</a>"
  __stream__
end

つまり、動的な要素をどのようにテンプレートに埋めこむかについては、Filterが自由にコントロールできるので、いろいろなことができます。

詳しくは次のソースを参照してください。

http://retro.brain-tokyo.net/projects/amrita2/browse/trunk/amrita2/specs/filters.rb

また、今後、最適化の為のフィルターを追加して性能を改善してく予定です。与えるデータの形式が決まっているのであれば、それ専用のフィルターを作ることで、ほとんどの場合に最速のRubyコードを出力できるはずです。

  1. 普通に書きやすい形でテンプレート作成
  2. ホットスポットを見つける
  3. 作者(私)に「このテンプレートから、これこれのRuby コードを吐くフィルタ作ってくれ」と頼む
  4. 作者が作ったフィルタを受け取ってインストール
  5. そのフィルタの指定をテンプレートに追加

この手順で、性能問題は解決できます。

データタイプ

Amrita2は、さまざまなRubyのビルトインタイプとユーザ定義のオブジェクトを受け取ることができます。言いかえれば、コンパイラはどのよようなデータを渡されるかをコンパイル時に知ることができません。

そこで、データを読み取る時には、amrita_valueというメソッドを使用するようになっています。

def render_with(x)
   "<span>" + x.amrita_value(:xxx) + "</span>"
end

amrita_valueは、たとえば、Hashクラスでは次のように定義されています。

def amrita_value(name)
  self[name.to_sym]
end

しかし、Structオブジェクトは、次のように定義された同じ名前の別のメソッドを使います。

def amrita_value(name)
   self.__send__(name)
end

Bindingクラスは、さらに別の定義を持っています。

class Binding
  def amrita_value(name)
    eval "begin; #{name}; rescue(NameError); @#{name};end;", self
  end
end

Bindingオブジェクトを渡すと、ローカル変数をテンプレートに埋め込むことができます。

t = Amrita2::Template.new('<strong am:src="aaa" />')
aaa = 'Amrita2'
t.render_with(binding)  # => "<strong>Amrita2</strong>" 

詳しくは下記を参照してください。

http://retro.brain-tokyo.net/projects/amrita2/browse/trunk/amrita2/specs/datatypes.rb

インラインERb

ERbを含んだテンプレートの扱いについては、InlineERbを参照してください。

*1:だれか英語に(再)翻訳してくれる人いないかな