読者です 読者をやめる 読者になる 読者になる

[Gauche] 〜Gauche on Railsへの道〜 1. ERB風のものを作る

gauche.night で発表する Gauche on Rails ですが、やっと制作を開始しました ^^);

まずは、テンプレートエンジンの ERB 風のものを作って見ることにします。

Gauche/Kahua のテンプレート

ruby のeRuby(ERB...)のような標準的なテンプレートエンジン/ライブラリーは無いようです。Kahua(Gauche上に作られたWebフレームワーク)にはkahua.xml-templateというXML/XHTML用のテンプレートがあります。
また、Gauche にはtext.html-lite というHTML構築用のライブラリーがあり、以下の例のように S式からHTMLを作成する事ができます。

gosh> (tree->string (html:table (html:tr (html:td 1) (html:td 2))))

"<table><tr><td>1</td\n><td>2</td\n></tr\n></table\n>"

ERBの動作

さて、ERBですが、どうやってテンプレートから文字列に変換されるのでしょうか。
サンプルとして

<table>
 <% 10.times do |n| %>
  <tr><td> <%= n + 1%> </td></tr>
 <% end %>
</table>

を一行で書いた文字列がどうERBで処理されるのか irb で調べてみます。

% irb 
>> require 'erb'
=> true
>> $e = ERB.new "<table>\n<% 10.times do |n| %>\n<tr><td><%= n + 1%> </td></tr>\n<% end %>\n</table>\n"
=> #<ERB: ..... 省略 
>> $e.src   
=> "_erbout = ''; _erbout.concat \"<table>\\n\"\n 10.times do |n| ; _erbout.concat \"\\n\"\n_erbout.concat \"<tr><td> \"; _erbout.concat(( n + 1).to_s); _erbout.concat \" </td></tr>\\n\"\n end ; _erbout.concat \"\\n\"\n_erbout.concat \"</table>\\n\"\n_erbout"

$e.src はテンプレートを ruby の文に変換した文字列です。この文字列を eval する事でテンプレートを展開した文字列が得られます。
$e.src をもう少し読みやすく書いてみると

_erbout = ''
_erbout.concat "<table>\n"
10.times do |n| 
   _erbout.concat "<tr><td> "
   _erbout.concat(( n + 1).to_s)
   _erbout.concat " </td></tr>\n"
end
_erbout.concat "</table>\n"
_erbout

となります。<% %> の中は ruby の文なのでそのまま、<%= %>の中はRuby の式なのでその値を文字列に変換し _erbout変数に連結、その他の部分は文字列として_erbout変数に連結。 最後に _erbout変数の値を戻すようになっています。

Gauche on Rails では

Gauche on Rails では eRubyと同じような感じです、ただし <% %>の中が Gauche の式になります。以下は上の ERB と同じ文字を表示するテンプレートです。

<table>
 <% (dotimes (n 10) %>
  <tr><td> <%=(+ 1 n)%> </td></tr>
 <% ) %>
</table>

これをテンプレートエンジン egosh関数が変換すると、以下のような Gauche(Seheme)の式になります。

(call-with-output-string (lambda(p)
  (display "<table>\n" p)
  (dotimes (n 10)
    (display "  <tr><td>" p)
    (display (+ n 1) p)
    (display "</td></tr>\n" p))
  (display "<table>\n" p)))


コードは以下のようになりました。 Gauche(Scheme,Lisp)でのプログラミングは、まだまだです・・・・

(define (egosh template)
  (let ((port (open-output-string)))
    (define (compile-elem templ)
      (cond ((string=? templ "") #t)
	      ((#/^<%=(.+?)%>/ templ) =>
	       (lambda(m)
		 (display #`"(display ,(m 1))" port)
		 (compile-elem (m 'after))))
	      ((#/^<%(.+?)%>/ templ) =>
	       (lambda(m)
		 (display #`",(m 1)" port)
		 (compile-elem (m 'after))))
	      ((#/^(.+?)<%/ templ) =>
	       (lambda(m)
		 (display #`"(display \",(m 1)\")" port)
		 (compile-elem (string-append "<%" (m 'after)))))
	      (else (display #`"(display \",templ\")" port))))
    (display "(call-with-output-string (lambda(p)" port)
    (compile-elem template)
    (display "))" port)
    (read-from-string (get-output-string port))))

今回、覚えた事や感想

  • (cond (条件 => 式) ... という書き方をすると、「条件」の評価結果を「式」で使う事ができて便利。
  • Gauche にはPerlRubyと同等の正規表現があって、Perl/Ruby プログラマーには嬉し。
  • Gauche には適用可能なオブジェクトという機構があり、このおかげて正規表現にマッチした部分の結果文字列等の扱いが簡潔に書ける。
  • Perlの "abc $x def", Ruby の "abc #{x} def" と同様の事が #`"abc,x def" と書ける。
  • ERBではconcatメソッドで文字を繋いでいるが、Gauche には文字列の破壊的結合関数はなく大きな文字列を組み立てるのは、文字列に対するI/O操作的な方法ポートを使う。
  • 現在のegosh関数では内部的に文字列でS式を組み立て最後に read-from-string 関数で S式に変換している。これは <% %> の中は繰り返しの開始部分のように、完全なS式ではないので単純に リストにS式を連結していくやり方では上手く行かないからです。 しかし能率は悪いし、カッコも良くない・・・・ もう少し勉強したら良いやり方が分かるかも?