モダンPerlの世界へようこそ

第43回Text::Xslate:永続環境に特化したテンプレートエンジン

TTの本体を差し替える

前回はウェブ業界で標準的に使われているTemplate Toolkitをより安全に使うためのカスタマイズ方法をいくつか紹介しました。しばしば批判の対象となってきたエスケープの問題については、TTでも適切な拡張を施せば後発のモジュールと遜色ないか、それ以上に便利に使えることは確認できたかと思います。

ただし、エスケープの仕方ひとつとってもさまざまなやり方があったように、TTは、柔軟である代償として速度面ではかなりの不利を抱えています。

もっとも、不利といってもそれはいまの、しかもかなり規模の大きな現場の視点で見たときの話で、数年前、おもなライバルがHTML::Mason(と、機能面で大きな差があるHTML::Templateだった時代にはTTも十分に高速といえましたし[1]⁠、中小規模のサイトではいまでもTTで十分なレスポンスは得られます。

また、かれこれ10年近くもデファクトスタンダードとして君臨してきただけに、TTは、Perlのコードを書く人だけでなく、HTMLやCSSが専門の人や、設計や運用を担当している人の間でもノウハウの蓄積を期待できます。CPAN上での開発は停滞気味とはいえ、基本的な部分はもう十分に枯れていますし、必要に応じて好きなように拡張もできるのですから、現状に特に不満を感じていないのであれば、慣れたTTを使い続けたからといって何の問題もありません。

そのような前提を共有したうえで、たとえばソーシャルゲームのように本当に速度が必要とされる現場や、安価なVPSなど速度はともかく省メモリが求められる環境などで、どうしてもTTの遅さや大きさが問題になってきたときにどうするか、というのが今回のテーマ。

前回はあくまでもTTの枠組みのなかで工夫を重ねてみましたが、今回は、移行コストも考慮して、テンプレートにはなるべく手をつけずに、エンジン部分をまるごと取り替えることで高速化をはかる方法を紹介してみます。

Template::Alloy

TTとテンプレートレベルでは(ほぼ)互換性を持つエンジンの例としては、まずポール・シーモンズ(Paul Seamons)氏が2007年5月にリリースしたTemplate::Alloyがあげられます。

これはもともと氏が2003年末に開発を始めたCGI::ExというCGI用のフレームワークに付属していたテンプレートエンジン(CGI::Ex::Template)から派生したもので、当初はTTをそのままベースクラスとして利用していたのですが、2006年5月にリリースされたCGI::Ex 2.0でエンジン部分をまるごと書き換え、TTのテンプレートをそのまま扱える独自のエンジンに生まれ変わりました。

この新しいCGI::Ex::Templateは、ろくにキャッシュのきかない安価なCGI環境などでは、素のTTはもとより、Template::Stash::XSを使って高速化したものよりも速度を稼げるようになったのですが[2]⁠、その後、氏が転職とともにTT以外のエンジンも利用するようになり、HTML::Templateなどの記法への対応も追加されたことから、CGI::Ex::Templateは単純にTTを高速化した代用品というより、複数のテンプレートエンジンのインタフェースを共通化したものという扱いを受けるようになります。それを、いかにもCGI環境でしか使えないかのように誤解させる名前空間の中に入れておくのはもったいないということで、議論の末に「合金」という意味の名前をつけて独立させたのがTemplate::Alloyの起源です。

Template::Alloyは、もともとTTから派生したものだけに、基本的な使い方はTTとほとんど変わりません。プラグインについても、TTがインストールされている環境であれば、TTのものをそのまま流用できます。

use strict;
use warnings;
use Template::Alloy;

my $ta = Template::Alloy->new;

$ta->process(\(my $tmpl =<<'TMPL'), {foo => '<"bar">'}) or die $ta->error;
[% USE JavaScript %]
[% foo %]
[% foo | html | js %]
TMPL

毎回htmlフィルタを書くのが面倒であれば、AUTO_FILTERに設定しておけば自動的にフィルタがかかるようになります。自動フィルタを適用したくない項目についてはnoneフィルタをあてておけば素の値を埋め込めます。

my $ta = Template::Alloy->new(AUTO_FILTER => 'html');

Template::Alloyでは、TT2の構文だけでなく、ほかのテンプレートから流用したものを含め、独自に拡張した構文を利用することもできます。

use strict;
use warnings;
use Template::Alloy;
use Template;

my $tmpl =<<'TMPL';
[%# Alloyでは複数の範囲を指定できます -%]
[% FOR i IN [1..2, 4..5] -%]
  [%- i -%]
[% END -%]
TMPL

my $ta = Template::Alloy->new;
my $tt = Template->new;

$ta->process(\$tmpl) or die $ta->error;
$tt->process(\$tmpl) or die $tt->error; # エラー

その他、細かな挙動の違いについてはTemplate::Alloy::TTのドキュメントに詳しく説明されています。また、一般的な使い方についても、Template::Alloyのドキュメントだけ読めばわかるくらいしっかりまとめられているので、TTにあまりなじみがなくても移行は楽です。

リポジトリやメーリングリストが公開されていないなど、サポート面にはやや難もありますが、2011年1月にリリースされた1.016ではセキュリティを高めるために上で紹介したAUTO_FILTERの設定が追加されるなど、作者氏のレスポンスは悪くないようですから、用途があうなら使ってみるのもよいでしょう。

Template::Tiny

この文脈では、アダム・ケネディ(Adam Kennedy)氏が2009年末にリリースしたTemplate::Tinyの名前があがることもあります。これは::Tinyシリーズの常として、いろいろと制限の厳しい環境用に必要最小限のコードでTTの機能(のごく一部)を再実装しようというもので、正味のコードは200行ほどしかありませんから取り回しは簡単ですし、例によって非永続環境では素のTTや、場合によってはTemplate::Alloyよりも高速になることもあるのですが、繰り返し実行したときの性能劣化が激しく、永続環境ではTTほどの性能は出ませんし、何よりテンプレートの制約が大きすぎるため、TTの長所がほとんど生きてこないのが泣き所。ドット構文を使って複雑なデータ構造やオブジェクトをそのまま埋め込めるのはよいのですが、なにしろテンプレートレベルではHTML用のフィルタひとつかかりませんので、埋め込みたい値はあらかじめ自分でエスケープしてやらなければなりません。興味深い実装ではあるものの、::Tinyシリーズの常として、よほどの事情がなければ使う理由はないといってもよいでしょう。

Text::Xslate

gfxこと藤吾郎氏のText::Xslateについては、2010年のYAPC::Asiaなどでも紹介がありましたし、gihyo.jpでもWEB+DB PRESSの連載記事が公開されているので、すでにご存じの方も多いことでしょう。Xslateは、標準ではKolonと呼ばれる独自のテンプレート言語を使うようになっていますが、初期化時に設定を変えることでTTとかなり親和性の高い環境を作ることもできます。

use strict;
use warnings;
use Text::Xslate;
my $tx = Text::Xslate->new(
    syntax => 'TTerse',
    module => ['Text::Xslate::Bridge::TT2Like'],
);

TTerseというのはText::Xslateに同梱されているText::Xslate::Syntax::TTerseというTT風の構文をサポートするためのモジュールで、あまり複雑なことをしないのであればこれだけでも足りるのですが、TTerseはTTの仮想メソッドをそれほど多くはサポートしていないので、たいていはCPANからText::Xslate::Bridge::TT2Likeというモジュールをインストールして、TT風の仮想メソッドを追加してやる必要があります[3]⁠。

Text::Xslateのキャッシュ

Text::Xslateはもともと速度を稼ぐために大半がXS/Cで書かれていますが、いっそうの高速化のために、標準ではホームディレクトリ直下の.xslate_cacheディレクトリにキャッシュを保存するようになっています。このようにデフォルトのキャッシュディレクトリがシステムレベルで固定されているため、Xslateを使っているアプリケーションが複数ある環境では初期化時にcache_dirを適切に設定しておくのが無難です。

use FindBin;

my $tx = Text::Xslate->new(
  syntax => 'TTerse',
  module => [qw/Text::Xslate::Bridge::TT2Like/],
  cache_dir => "$FindBin::Bin/.cache",  # など
);

なお、どうせ一回こっきりしか実行しないスクリプトだからと思ってキャッシュを切ってしまうと、Xslateの実行速度は素のTTにも劣るほど遅くなります。Xslateを使う場合は(テスト目的でない限り)キャッシュを切らないようにしましょう。

Text::XslateとTTの違い

TTとTerseの違いについては、前掲のWEB+DB PRESS/gihyo.jpの記事にざっくりまとめられているほか、Text::Xslate::Syntax::TTerseやText::Xslate::Manual::FAQのPODにもいくらか記載があります。

一例として、前回取り上げたJavaScriptのエスケープの例を考えてみると、TTではインスタンス(ビュークラス)の設定はごく汎用的なものにとどめて、USEディレクティブを利用して動的にプラグインを読み込むようなことができましたが、Xslateの場合はUSEを使った動的なプラグインの読み込みは禁止されているので、XslateのインスタンスをつくるときにあらかじめJS用のフィルタを登録しなければなりません。

Text::Xslate::Manual::Cookbookにも名前があがっているkazeburoこと長野雅広氏のJavaScript::Value::Escapeを使うと、このような感じになるでしょうか。

use strict;
use warnings;
use FindBin;
use Text::Xslate;
use JavaScript::Value::Escape ();

my $tx = Text::Xslate->new(
  syntax => 'TTerse',
  module => [qw/Text::Xslate::Bridge::TT2Like/],
  cache_dir => "$FindBin::Bin/.cache",
  function => {
    js => \&JavaScript::Value::Escape::js,
  },
);

print $tx->render_string(<<'TMPL', { var => q/<foo>/ });
<script>
document.write('[% var | html | js %]');
</script>
<a onclick="alert('[% var | js %]')">alert</a>
TMPL

設定時に関数を定義する必要があるため、Xslateの設定をYAMLやJSONなどのファイル経由で読み書きするのはやや面倒になっていますが、上の例のように外部モジュールの方でエクスポートできる関数については、moduleの設定を調整することで対応する手もあります。

my $tx = Text::Xslate->new(
  syntax => 'TTerse',
  module => [
    'Text::Xslate::Bridge::TT2Like',
    'JavaScript::Value::Escape' => [qw/js/],
  ],
  cache_dir => "$FindBin::Bin/.cache",
);

また、XslateはTTと違ってデフォルトでHTML用のフィルタがかかりますが、適用順序によってはXSSを防ぎきれないため、document.writeやinnerHTMLに埋め込むときはXslateに標準で用意されているhtmlフィルタを明示的に追加するか、htmlフィルタの抜けを防ぐために登録する関数を以下のような形にしておく方法が知られています。

my $tx = Text::Xslate->new(
  syntax => 'TTerse',
  module => [qw/Text::Xslate::Bridge::TT2Like/],
  cache_dir => "$FindBin::Bin/.cache",
  function => {
    js => sub {
      JavaScript::Value::Escape::js(
        Text::Xslate::Util::escape_html(@_)
      )
    },
  },
);

Text::XslateはTTのかわりになれるのか?

Xslateは、適切に使えばTTよりもはるかに高速です。この点については、Xslateが話題になるたびに強調されていますし、テンプレートのベンチマーク結果などを見れば(いささか恣意的な部分はあるにせよ)誰しも納得できることです。

ただし、単に速さを強調するだけで使ってくれるのは、流行り物が好きな一部のユーザくらいのものです。国内ではYAPCやPerl Mongersグループのイベントに集まってくるような積極的なユーザによる導入事例もちらほら見られますが、世界的には(過去に何度か宣伝は行われているものの)いまのところほとんど関心を持たれていないといってもよいでしょう[4]⁠。

その理由の一部はもちろん英語の壁にあるのでしょうが、ことテンプレートエンジンに関しては、それだけで片付けるわけにはいきません。

前回、TTがデファクトスタンダードとみなされるにいたった理由のひとつとして、宮川達彦氏らによるSledgeや、Perl.comの管理人として知られていたサイモン・カズンズ(Simon Cozens)氏のMaypoleその後を継いだゼバスティアン・リーデル(Sebastian Riedel)氏のCatalystといったウェブアプリケーションフレームワークの存在をあげました。Sledgeはもともと(現在のライブドア社の前身にあたる)オン・ザ・エッヂ社の社内フレームワークでしたし、Maypoleも実質的な開発期間は1年に満たないほどでしたから、それほど多くのユーザを獲得できたわけではありませんが、いまもなおTTがデファクトスタンダードの地位を守り続けていられるのは、15年にも及ぶ歴史や、アナグマ本のような書籍が存在しているからだけでなく、このような有名なフレームワークの開発者やユーザが(ときに看板を付け替えながらも)長年にわたってTTのサポートをしてきたからでもあります。

とりわけ近年は、いわゆるモダンPerlをビジネスと結びつけようとしてきたグループがさまざまなところで(自分たちがビジネスの道具として売り込もうとしているCatalystの標準テンプレートとしての)TTの疑問に答える努力を続けてきました。その善し悪しはまた項をあらためて検証するとして、Catalystを入れればほとんど必然的についてくるうえ、躓いたときにはCatalystのコミュニティがサポートしてくれるTTと、性能的には上らしいけれども、英語ではほとんどサポートがなく、既存のTTやCatalystとの互換性もあやしいXslateとでは、まともな勝負になるはずもありません。まして、Xslateの場合は、同じ藤吾郎氏がメンテナンスを続けているMouseと、Catalystチームが採用しているMooseという、非常にわかりやすい比較の対象もあります。彼らにしてみれば、例によって空気を読めない日本人が速いばかりの類似品を売り出そうとしているけれど、時期がくれば本家がそれ以上のものになるだろうからいちいち相手にする必要はない、というのが正直なところでしょう。

今回の記事でも、TTとの互換性が考慮されていないものについては、ほかにどれだけ見るべきところがあっても黙殺しましたが、特定の企業、地域、グループといった小さな枠組みを超えた世界への普及を目指すのであれば、性能を誇示するだけでなく、競合からの移行コストを最小限にするなどの工夫が不可欠です。Xslateの場合も、たとえばCookbookの記述をTTerseベースにしたり、既存のCatalystアプリのテンプレートをながめて、よく使われているにもかかわらずXslateではサポートされていない書き方をどう移行すればよいかまとめたりするだけでもずいぶん印象は変わるでしょうし、XslateはデフォルトでHTML用のフィルタがかかることからもわかるように基本的にはウェブに特化したテンプレートエンジンですから、ユーザが増えるかどうかは、汎用性の高いTT以上に、採用してくれるフレームワークの数や、そのユーザ数にかかっています。その意味ではXslate単体で売り込むよりは、tokuhiromこと松野徳大氏のAmon2のようにXslateを積極的に利用しているフレームワークを大々的に売り込んでいくほうが早道かもしれませんし、TT3が足踏みしている間にCatalystチームにXslateの有用性を認めさせることができれば、あとは芋づる式にユーザが増えていくことも期待できます。

もちろんこのようなビジネス的な施策はかならずしもXslateの開発陣が行わなければならないことではありません。ビジネスの道具としてであれ、それ以外の理由であれ、Xslateの普及を進め、その結果、Xslate風のテンプレートを書いたり、Xslateを拡張したりすることのできる開発者やデザイナの数を増やしたいという欲を持つ個人ないし団体が行えばよいことです。

ただ、いわゆる「モダンPerl⁠⁠、とりわけビジネスの世界におけるモダンPerlというのは、⁠TMTOWTDI(やり方はひとつではない)というPerlのモットーが選択肢の乱立につながり、初学者にはベストプラクティスが見えづらくなっている」という批判に対するひとつの回答、という側面もあります。ことウェブの世界に限ってしまえば、⁠Rubyには(ビジネスの世界で大成功した)Railsがあるけれど、Perlには……?」という問いに答えるための努力という言い方もできるでしょう。

モダンPerlの世界も一枚岩ではありませんので、この連載ではあえてその流れを無視して複数の選択肢を取り上げている部分もありますが、いずれにしてもその場の空気を無視していたずらに独自仕様を宣伝するだけでは、そんなノイズを拡散する前に「本家」の改善に協力しなさいよと言われても仕方ありません。もちろんガラパゴスと言われるのを覚悟で独自路線を行くのもひとつのやり方ではありますが、その問題点については、おそらく多くの方がケータイなどの世界を通じて承知しているはずです。

次回はテンプレートの話を少し離れて、そのようなモダンPerlの世界における政治的な話を少しまとめてみようと思います。

おすすめ記事

記事・ニュース一覧