Template Toolkit 探訪 (1)

Perl でドキュメントテンプレートシステムを利用したい場合,

  1. 変数の展開を使う(ex. print "Hello, ${world_name}\n";)
  2. 正規表現の置換を使う
  3. HTML::Template を使う
  4. Template (Toolkit) を使う

あたりが選択肢となるかと思います。
3番目の HTML::Template は,内的には2番目の置換を使っているだけなのでわりとシンプルな仕組みです。できることもわりとシンプルで縛りも多いですが,ロジックとビューの分離という点では縛りのせいで逆に明確にわけることができて,結構人気が高いようです。
4番目の Template Toolkit(以下 TT)はとても高機能で,それゆえ一部の層に人気です。ベンチマーク結果 によると(デフォルトの設定では) HTML::Template には勝ててはいないようですが,高機能な割には十分高速だと思います。

TT の高速性は XS を持っているからだ,と誤解されている向きもありますが,実際には「Template-ToolkitはPure Perlでも動く : blog.nomadscafe.jp」のように,Stash の部分で XS を使っているだけであり,ほとんどの部分は Pure Perl で書かれています。TT のキモは,

  • テンプレートドキュメントをパーサで Perl オブジェクトにコンパイルする
  • コンパイルしたドキュメントは(デフォルトでは)メモリに貯蔵しておき,再利用する

という点にあります。

つまり,たとえば

This is [% myval %].

というテンプレートがあった場合,これはコンパイルされて,

sub {
  "This is " . $myval . ".";
}

のような無名サブルーチンに落とし込まれるということです。ですから,パーシングには時間がかかるものの,実際に展開する局面では HTML::Template などの正規表現置換系なみに高速に動作する,はず,です。

今回はこの実際のコンパイルされたテンプレートドキュメントをみてみたいと思います。


まずは,先ほどのサンプルをスクリプトに落とし込んでみます。

#!/usr/bin/perl

use strict;

use Template;
use Template::Context;

my $tc = Template::Context->new;
my $td = $tc->template(\*DATA);

print $tc->process($td, { myval => 'test' });

__END__
This is [% myval %].

ドキュメントオブジェクトををとりだしやすいように,Template ではなく Template::Context を使っています。この実行結果は,もちろん,

This is test.

となります。
さて,コンパイルされたドキュメントオブジェクトをみてみましょう。「__END__」の前に,

use Data::Dumper;

print Dumper($td);

を付け加えてみましょう。

$VAR1 = bless( {
  '_DEFBLOCKS' => {},
  '_BLOCK' => sub { "DUMMY" },
  'callers' => [],
  'modtime' => 1146287604,
  'name' => 'input file handle',
  '_HOT' => 0
}, 'Template::Document' );

インデントは変えてありますが,このとおり,Template::Document というクラスのオブジェクトになっていることがわかります。
_BLOCK の「sub { "DUMMY" }」とあるのは,Data::Dumper がサブルーチンリファレンスをこのように表現しただけなので,本当の中身ではありません。

このサブルーチンの中身を B::Deparse を使って見てみましょう。「_BLOCK」は $td->block() でアクセスできるので,さらにコードを付け加えてみます。

use B::Deparse;

my $b = B::Deparse->new(qw(-P -sC));
print $b->coderef2text($td->block);

詳しくは B::Deparse のマニュアルを見てください。この実行結果は,

{
    package Template::Document;
    use strict 'refs';
    my $context = shift @_ || die("template sub called without context\n");
    my $stash = $context->stash;
    my $output = '';
    my $error;
    eval {
        do {
            BLOCK: {
                $output .= 'This is ';
                $output .= $stash->get('myval');
                $output .= ".\n";
            }
        }
    };
    if ($@) {
        $error = $context->catch($@, \$output);
        die $error unless $error->type eq 'return';
    }
    return $output;
}

となります。
ご覧の通り,例外処理の処理でやや冗長になっていますが,テンプレートの内容が Perl コードに落とし込まれていることがわかります。