最近社内でvim勉強会があったりして「vimもいいなぁ」と思っていたのですが、やっぱりキーバインドに慣れなくってEmacsに戻ってきました。最近EmacsでPerl関連の設定を見なおしたので、メモがてら書いておきます。自分がPerlのコードを書くときに使っているelispは以下の5つです。
- cperl-mode
- flymake + set-perl5lib
- anything + auto-complete + perl-completion
- yasnippet
これらを一つ一つ紹介していきます。
cperl-mode
cperl-mode はこんな感じで設定しています。とりあえず色付けと適切なインデントがなされればいいかなという感じ。
(autoload 'cperl-mode "cperl-mode" "alternate mode for editing Perl programs" t)
(add-to-list 'auto-mode-alist '("\\.\\([pP][Llm]\\|al\\|t\\|cgi\\)\\'" . cperl-mode))
(add-to-list 'interpreter-mode-alist '("perl" . cperl-mode))
(add-to-list 'interpreter-mode-alist '("perl5" . cperl-mode))
(add-to-list 'interpreter-mode-alist '("miniperl" . cperl-mode))
;;; cperl-mode is preferred to perl-mode
;;; "Brevity is the soul of wit" <foo at acm.org>
(defalias 'perl-mode 'cperl-mode)
(setq cperl-indent-level 4
cperl-continued-statement-offset 4
cperl-close-paren-offset -4
cperl-label-offset -4
cperl-comment-column 40
cperl-highlight-variables-indiscriminately t
cperl-indent-parens-as-block t
cperl-tab-always-indent nil
cperl-font-lock t)
(add-hook 'cperl-mode-hook
'(lambda ()
(progn
(setq indent-tabs-mode nil)
(setq tab-width nil)
; perl-completion
(require 'auto-complete)
(require 'perl-completion)
(add-to-list 'ac-sources 'ac-source-perl-completion)
(perl-completion-mode t)
)))
; perl tidy
; sudo aptitude install perltidy
(defun perltidy-region ()
"Run perltidy on the current region."
(interactive)
(save-excursion
(shell-command-on-region (point) (mark) "perltidy -q" nil t)))
(defun perltidy-defun ()
"Run perltidy on the current defun."
(interactive)
(save-excursion (mark-defun)
(perltidy-region)))
(global-set-key "\C-ct" 'perltidy-region)
(global-set-key "\C-c\C-t" 'perltidy-defun)
flymake + set-perl5lib
flymake はEmacs自体に含まれているソースコードチェッカーです。set-perl5lib はここからダウンロードできます。この設定を入れるとコードを書いているときに裏でsyntax checkが走って、エラーになっている箇所を赤くしてくれます。これのおかげでしょうもないtypoに簡単にきづけるので、だいぶコードを書くのが速くなりました。かなりおすすめの設定です。

なお、自分は C-c e でエラーが発生している箇所にジャンプしてエラーメッセージをミニバッファに表示するように設定しています。
;; flymake for perl
(defvar flymake-perl-err-line-patterns '(("\\(.*\\) at \\([^ \n]+\\) line \\([0-9]+\\)[,.\n]" 2 3 nil 1)))
(defconst flymake-allowed-perl-file-name-masks '(("\\.pl$" flymake-perl-init)
("\\.pm$" flymake-perl-init)
("\\.t$" flymake-perl-init)
))
(defun flymake-perl-init ()
(let* ((temp-file (flymake-init-create-temp-buffer-copy
'flymake-create-temp-inplace))
(local-file (file-relative-name
temp-file
(file-name-directory buffer-file-name))))
(list "perl" (list "-wc" local-file))))
(defun flymake-perl-load ()
(interactive)
(set-perl5lib)
(defadvice flymake-post-syntax-check (before flymake-force-check-was-interrupted)
(setq flymake-check-was-interrupted t))
(ad-activate 'flymake-post-syntax-check)
(setq flymake-allowed-file-name-masks (append flymake-allowed-file-name-masks flymake-allowed-perl-file-name-masks))
(setq flymake-err-line-patterns flymake-perl-err-line-patterns)
(flymake-mode t))
(add-hook 'cperl-mode-hook '(lambda () (flymake-perl-load)))
(defun next-flymake-error ()
(interactive)
(flymake-goto-next-error)
(let ((err (get-char-property (point) 'help-echo)))
(when err
(message err))))
(global-set-key "\C-ce" 'next-flymake-error)
auto-complete + perl-completion
auto-complete と perl-completion を組み合わせてPerlのモジュールや関数、変数を補完する設定をしています。設定する elisp は先ほどの cperl-mode のところに含まれているので割愛します。下記のような感じで補完候補が出てきます。

また、perl-completion にはデフォルトで C-c s するとカーソルのあるモジュールのドキュメントをanything インタフェースで開くことができます。

yasnippet
yasnippet は任意のキーワードを打ってTABを押すと、そのキーワードを登録しておいたテンプレート(snippet)で置換してくれるものです。うまく使えばかなりのタイプ量が減らせるのでコードを書くのが早くなります。自分は例えば
use strict;
use warnings;
というsnippetを “usestwa” というキーワードで登録しています。
yasnippetには予め用意されているものがあるのですが、自分はそれとは別にディレクトリを作成して(~/.emacs.d/mysnippets)、そこに自分で作ったsnippetを置いています。以下がyasnippetの設定です。
(require 'yasnippet)
(require 'dropdown-list)
(setq yas/text-popup-function #'yas/dropdown-list-popup-for-template)
;; コメントやリテラルではスニペットを展開しない
(setq yas/buffer-local-condition
'(or (not (or (string= "font-lock-comment-face"
(get-char-property (point) 'face))
(string= "font-lock-string-face"
(get-char-property (point) 'face))))
'(require-snippet-condition . force-in-comment)))
;; yasnippet 公式提供のものと、
;; 自分用カスタマイズスニペットをロード同名のスニペットが複数ある場合、
;; あとから読みこんだ自分用のものが優先される。
;; また、スニペットを変更、追加した場合、
;; このコマンドを実行することで、変更・追加が反映される。
(defun yas/load-all-directories ()
(interactive)
(yas/reload-all)
(mapc 'yas/load-directory-1 my-snippet-directories))
;;; yasnippet展開中はflymakeを無効にする
(defvar flymake-is-active-flag nil)
(defadvice yas/expand-snippet
(before inhibit-flymake-syntax-checking-while-expanding-snippet activate)
(setq flymake-is-active-flag
(or flymake-is-active-flag
(assoc-default 'flymake-mode (buffer-local-variables))))
(when flymake-is-active-flag
(flymake-mode-off)))
(add-hook 'yas/after-exit-snippet-hook
'(lambda ()
(when flymake-is-active-flag
(flymake-mode-on)
(setq flymake-is-active-flag nil))))
(setq yas/root-directory (expand-file-name "~/.emacs.d/snippets"))
;; 自分用スニペットディレクトリ(リストで複数指定可)
(defvar my-snippet-directories
(list (expand-file-name "~/.emacs.d/mysnippets")))
(yas/initialize)
(yas/load-directory "~/.emacs.d/snippets")
(yas/load-all-directories)
まとめ
以上の設定をすると
- ソースの色付け
- シンタックスチェック
- シンボルの補完
- モジュールのドキュメントを引く
- snippetによるタイプ量の削減
ができるようになります。EclipseのようなIDEにはまだ及びませんが、これでだいぶコードを書くのが速くなるかと思います。
情報源
を参考にさせてもらいました。ありがとうございます。
Perlでコマンドラインオプションを解析する場合、大体は標準添付されているGetopt::Longを使うと思いますが、自分は3年前ぐらいから Getopt::Compact というモジュールに出会い、それ以降大抵の場合はこれを使っています。このモジュールの良いところは
- オプションの定義(-c, –config などの定義)とそのヘルプメッセージが一箇所で定義できる
- -h, –helpオプションを最初から定義してくれてる(–manオプションも)
- 定義してないオプションが指定されると自動的にエラーにしてくれる
- Getopt::Compact#usage でヘルプメッセージが簡単に取得できる
があると思っています。などと抽象的な話をしてもしょうがないので、具体的なコードを書いてみます。
#!/usr/bin/env perl
use strict;
use warnings;
use Getopt::Compact;
use Data::Dumper;
my $getopt = Getopt::Compact->new(
name => 'getopt_compact.pl',
modes => [ qw(verbose) ],
args => 'FILE',
version => '0.1',
struct => [
[ [ qw(f force) ], 'force overwrite of output file and compress links' ],
[ [ qw(l level) ], 'compress level', '=i' ],
[ [ qw(S suffix) ], 'use suffix on compressed files', '=s' ],
]
);
my $options = $getopt->opts;
my $file = shift @ARGV;
unless ($file) {
print STDERR $getopt->usage;
exit 1;
}
print "options -----\n";
print Dumper $options;
上の例では gzip のオプションのモノマネで
- -f, –forceというフラグオプション
- -l, –levelという数値のオプション
- -S, –suffixという文字列のオプション
- オプション以外に引数FILEを受け取る
という仕様としています。
でで、例えば
$ perl getopt_compact.pl --help
と実行すると
getopt_compact.pl v0.1
usage: getopt_compact.pl [options] FILE
options
-h, --help This help message
-v, --verbose Verbose mode
-f, --force Force overwrite of output file and compress links
-l, --level Compress level
-S, --suffix Use suffix on compressed files
--man Display documentation
のようにヘルプが表示されます。また、–hoge のような存在しないオプションを指定すると
$ perl getopt_compact.pl --hoge
Unknown option: hoge
getopt_compact.pl v0.1
usage: getopt_compact.pl [options] FILE
options
-h, --help This help message
-v, --verbose Verbose mode
-f, --force Force overwrite of output file and compress links
-l, --level Compress level
-S, --suffix Use suffix on compressed files
--man Display documentation
のようにエラーになりヘルプが表示されます。
という感じで、Getopt::Longより依存モジュールがあるものの便利に使わせてもらってます。ただ、PerlのGetopt系のモジュールは他にも色々あるのでもっと便利なのがありそうですが、こういうのもあるよという紹介でした。
DBIのちょっとしたTIPSです。DBIには connect する時に
DBI->connect('dbi:mysql:database=hoge', 'root', 'whatever', { ... });
とオプションを渡すことができます。(たとえば RaiseError) 。このオプションのひとつに HandleError というものがあってデバッグにはなかなか便利なので紹介してみます。端的にいうと HandleError でサブルーチンを登録しておくと、エラーが発生したときにこのサブルーチンを使ってエラーを投げてくれるようになります。以下は具体例。
# MyDB.pm
package MyDB;
use strict;
use warnings;
use Carp ();
use DBI;
sub new {
bless {}, shift;
}
sub connect {
my ($self) = @_;
$self->{dbh} = DBI->connect(
'dbi:mysql:database=dbix_thin_test', 'root', 'root',
{ RaiseError => 1, HandleError => \&Carp::confess } # here
);
}
sub select {
my ($self, $sql) = @_;
my $sth = $self->{dbh}->prepare($sql); # error!
$sth->execute();
}
1;
# dbi_handle_error.pl
#!/usr/bin/env perl
use strict;
use warnings;
use FindBin qw($Bin);
use lib "$Bin";
use MyDB;
my $db = MyDB->new;
$db->connect();
$db->select("select * from not_exist");
と書いてわざと MyDB#select でエラーになるようにしておき、このスクリプトを実行します。
すると
$ perl ~/script/perl/dbi_handle_error.pl
DBD::mysql::st execute failed: Table 'dbix_thin_test.not_exist' doesn't existDBI::st=HASH(0x248a900) at /home/kazuhiro/script/perl/MyDB.pm line 23
MyDB::select('MyDB=HASH(0x21b8df0)') called at /home/kazuhiro/script/perl/dbi_handle_error.pl line 11
のように DBI->connectの HandleError で渡した Carp::confess が呼ばれ、エラーになった時にスタックトレースが表示されます。HandleError を指定しない場合は
$ perl ~/script/perl/dbi_handle_error.pl
DBD::mysql::st execute failed: Table 'dbix_thin_test.not_exist' doesn't exist at /home/kazuhiro/script/perl/MyDB.pm line 23.
DBD::mysql::st execute failed: Table 'dbix_thin_test.not_exist' doesn't exist at /home/kazuhiro/script/perl/MyDB.pm line 23.
のようになってしまい、実際にエラーが発生している箇所と問題のクエリの関連性がわかりにくいですが、HandleErrorで Carp::confess を指定しておくとファイル名と行番号がわかるのでどこでエラーになっているかあたりがつけやすいと思います。(この例だとメソッドのネストが少ないのでそのありがたみがわからないですが…)
以上、生DBIでもこんなマニアックなオプションがあるんだよという紹介でした。ZIGOROuさんの生 DBI ユーザーのための DBI Cookbook (1)を見ててよく思うのですが、DBIってなかなか奥が深いです。
表題の通りですが、思うところがあってDBIx::Thinを削除しました。きっかけは、CPAN Ratingsのレビューです(以下引用)。
DBIx-Thin (0.05) **
It strikes me as odd how almost most of Japanese authors (of course, except honourable and admirable authors) don’t write why they have written their modules and the intention of the modules.
So is the author of this module.
The module is yet another ORM module but unfocused one. What the heck is different from DBIx::Skinny? The explanation is nowhere to be found, but there is just an enumeration of methods. And yet could anyone else use the module?
言われてみればその通りで、PODにコンセプトも書いてなければDBIx::Skinnyと違うところも良くわからないと。その辺に関してはモジュールのPODに書いておきましたが、それでもDBIx::Skinnyとあんまり変わらないし、現時点で自分以外の誰かが使っているとも思えないので、変な混乱を招かないようにCPANからは削除しました。んでgithubでは開発を継続していきます。
今年のYAPC(9月)からすき間時間を使って作っていたDBIx::Thinという、DBIx::Skinnyインスパイアなモジュールを昨日やっとリリースしました。作った動機は単に自己満足の追求と車輪の再発明による個人のスキルアップですが、前職で3年前ぐらいに自作したORMのコードを忘れないうちに改良して世に出したいなぁとずっと思っていたのです。SkinnyのAdvent Calendarが日々更新されているのを読み、途中で何度も「これSkinny使えばいいんじゃね?」って思って挫折しかけましたが、Skinnyも細かいところでは自分のポリシーと合わない部分があったりしたのと、とりあえず作ってしまったので世に出してみます。
特徴としては
- 生SQL派にやさしい
- Skinnyより書き方は冗長だと思うけど、コードを見て何をやっているかわかるようにした
- 依存モジュールが少ない(標準以外のものは3つ)
- Schemaクラスを生成するジェネレータが標準添付(ただしまだMySQL以外のDBに対応していない)
かなと思います。SQLはなんだかんだ言ってもまだWebエンジニアの共通言語だと思うので、ORMでゴリゴリJOINのコードを書くより生SQLの方がわかりやすい、と思っているので、生SQLなモジュールを作りました。
「Skinnyより書き方は冗長」というのは、例えばSkinnyだと
my $iterator = Your::Model->search(
'user',
{id => 1},
{order_by => 'id'}
);
と書きますが、2つ目の引数が何を表しているのかぱっと見わからないので、Thinの場合は
my $iterator = Your::Model->search(
'user',
where => {id => 1}, # これ
order_by => 'id',
);
と書くようにしています。もちろんSkinnyも学習すれば全然わかると思うので、細かいどうでもいいことかもしれませんが…
あとSkinnyと違う点は、
- SchemaクラスとレコードのRowクラスは分離されていますが、シンプルさを重視してあえて一緒のクラスにまとめているところ(Your::Model::UserみたいなクラスがSchema定義をしてさらにこれのインスタンスがRowオブジェクト)
- Schemaクラスの書き方がSkinnyはinflateやutf8がルールベースで全テーブルに適用されますが、こっちは基本的にテーブルの各カラムごとに書くようにさせてます
ところぐらいでしょうか。
最後に、Skinnyのコードはすごく綺麗で色々な部分を参考にさせてもらいました。作者のnekokakさん、どうもありがとうございました。
Perlで日付関連の処理をする代表的なモジュールにDateTimeというものがありますが、メモリ消費量が激しいのがずっと気になっていました。でで、Time::Pieceが5.10.1からPerlに標準添付になったという話を聞いて、乗り換えようかどうか検討しています。Perlメモ/Time::Pieceモジュール – Walrus, Digit.を見ると、DateTimeでできることは大体できるので、以下のユースケースでの速度面を測ってみます。
環境は以下で、ベンチマークのスクリプトは最後に載せてあります。
- OS: Ubuntu 9.04 amd64
- Perl: 5.10.0 x86_64-linux-gnu-thread-multi
- DateTime 0.51
- Time::Piece 1.15
use 時のメモリ使用量
こちらのgtop.plを使って測ります。
$ gtop.pl 'use DateTime'
10.2M : use DateTime
$ gtop.pl 'use Time::Piece'
2.5M : use Time::Piece
おおお、なんと4分の1!
現在日時でオブジェクトを生成する
$ ./benchmark_datetime.pl now
Benchmark: running now_datetime, now_time_piece for at least 3 CPU seconds...
now_datetime: 4 wallclock secs ( 3.03 usr + 0.00 sys = 3.03 CPU) @ 3149.83/s (n=9544)
now_time_piece: 2 wallclock secs ( 2.58 usr + 0.53 sys = 3.11 CPU) @ 52694.86/s (n=163881)
Rate now_datetime now_time_piece
now_datetime 3150/s -- -94%
now_time_piece 52695/s 1573% --
15倍!
日付オブジェクトに対する日付の加算
$ ./benchmark_datetime.pl add
Benchmark: running add_datetime, add_time_piece for at least 3 CPU seconds...
add_datetime: 3 wallclock secs ( 3.05 usr + 0.00 sys = 3.05 CPU) @ 1853.44/s (n=5653)
add_time_piece: 3 wallclock secs ( 2.76 usr + 0.42 sys = 3.18 CPU) @ 54969.81/s (n=174804)
Rate add_datetime add_time_piece
add_datetime 1853/s -- -97%
add_time_piece 54970/s 2866% --
Time::Pieceの圧倒的勝利。28倍!
日付オブジェクト同士の減算
$ ./benchmark_datetime.pl subtract
Benchmark: running subtract_datetime, subtract_time_piece for at least 3 CPU seconds...
subtract_datetime: 3 wallclock secs ( 3.00 usr + 0.00 sys = 3.00 CPU) @ 4065.00/s (n=12195)
subtract_time_piece: 4 wallclock secs ( 3.15 usr + 0.00 sys = 3.15 CPU) @ 132329.84/s (n=416839)
Rate subtract_datetime subtract_time_piece
subtract_datetime 4065/s -- -97%
subtract_time_piece 132330/s 3155% --
引き算もTime::Pieceの方が圧倒的に速いですね。
日付オブジェクト同士の比較
$ ./benchmark_datetime.pl compare
Benchmark: running compare_datetime, compare_time_piece for at least 3 CPU seconds...
compare_datetime: 3 wallclock secs ( 3.04 usr + 0.00 sys = 3.04 CPU) @ 47709.21/s (n=145036)
compare_time_piece: 4 wallclock secs ( 3.09 usr + -0.01 sys = 3.08 CPU) @ 173623.70/s (n=534761)
Rate compare_datetime compare_time_piece
compare_datetime 47709/s -- -73%
compare_time_piece 173624/s 264% --
日付オブジェクトを文字列にする
$ ./benchmark_datetime.pl stringify
Benchmark: running stringify_datetime, stringify_time_piece for at least 3 CPU seconds...
stringify_datetime: 3 wallclock secs ( 3.00 usr + 0.00 sys = 3.00 CPU) @ 44470.33/s (n=133411)
stringify_time_piece: 3 wallclock secs ( 2.45 usr + 0.66 sys = 3.11 CPU) @ 116681.35/s (n=362879)
Rate stringify_datetime stringify_time_piece
stringify_datetime 44470/s -- -62%
stringify_time_piece 116681/s 162% --
まとめ
総じてDateTimeよりTime::Pieceの方が高い性能を叩き出しました。速度にシビアな状況ではTime::Pieceを使った方が良いと感じました。こういう罠もあるみたいなので気をつけなくてはいけないところもありますが… インタフェースはどちらも綺麗に出来ているので使い勝手としては同じぐらいかなと思います。それにしてももっと速くTime::Pieceを検証しておけばよかったと思う今日この頃です。
ベンチマークスクリプト(benchmark_datetime.pl)
#!/usr/bin/env perl
use strict;
use warnings;
use Benchmark qw(cmpthese timethese);
use DateTime;
use Time::Piece;
use Time::Seconds;
my $timezone = DateTime::TimeZone->new(name => 'local');
#----------------------#
# now
#----------------------#
sub now_datetime {
my $now = DateTime->now(time_zone => $timezone);
}
sub now_time_piece {
my $now = localtime;
}
#----------------------#
# add
#----------------------#
my $add_dt = DateTime->now(time_zone => $timezone);
sub add_datetime {
$add_dt->add(days => 1);
}
my $add_tp = localtime;
sub add_time_piece {
$add_tp += ONE_DAY;
}
#----------------------#
# subtract
#----------------------#
my $sub_dt1 = DateTime->now(time_zone => $timezone);
my $sub_dt2 = DateTime->new(
year => 2008,
month => 12,
day => 1,
);
sub subtract_datetime {
my $dur = $sub_dt1->subtract_datetime($sub_dt2);
}
my $sub_tp1 = localtime;
my $sub_tp2 = Time::Piece->strptime('2008-12-01', '%Y-%m-%d');
sub subtract_time_piece {
my $sec = $sub_tp1 - $sub_tp2;
}
#----------------------#
# compare
#----------------------#
my $compare_dt1 = DateTime->now(time_zone => $timezone);
my $compare_dt2 = DateTime->now(time_zone => $timezone);
sub compare_datetime {
my $result = DateTime->compare($compare_dt1, $compare_dt2) <= 0;
}
my $compare_tp1 = localtime;
my $compare_tp2 = localtime;
sub compare_time_piece {
my $result = $compare_tp1 <= $compare_tp2;
}
#----------------------#
# stringify
#----------------------#
my $now_dt = DateTime->now(time_zone => $timezone);
sub stringify_datetime {
$now_dt->strftime("%Y-%m-%d");
}
my $now_tp = localtime;
sub stringify_time_piece {
$now_tp->strftime("%Y-%m-%d");
}
#----------------------#
# main
#----------------------#
my $mode = shift @ARGV || 'now';
my $count = shift @ARGV || -3;
if ($mode eq 'now') {
cmpthese timethese $count, {
'now_datetime' => \&now_datetime,
'now_time_piece' => \&now_time_piece,
};
} elsif ($mode eq 'add') {
cmpthese timethese $count, {
'add_datetime' => \&add_datetime,
'add_time_piece' => \&add_time_piece,
};
} elsif ($mode eq 'subtract') {
cmpthese timethese $count, {
'subtract_datetime' => \&subtract_datetime,
'subtract_time_piece' => \&subtract_time_piece,
};
} elsif ($mode eq 'compare') {
cmpthese timethese $count, {
'compare_datetime' => \&compare_datetime,
'compare_time_piece' => \&compare_time_piece,
};
} elsif ($mode eq 'stringify') {
cmpthese timethese $count, {
'stringify_datetime' => \&stringify_datetime,
'stringify_time_piece' => \&stringify_time_piece,
};
}
Now Text::MicroTemplate is even faster than HTML::Template::Pro – use GFx::WebLog;
Text::MicroTemplate 0.10で動作速度が改善されたとのことなので、自分のところでもベンチマークしてみました。
$ perl benchmark_templates.pl 1
Perl/5.10.0 (x86_64-linux-gnu-thread-multi)
HTML::Template/2.9
HTML::Template::Compiled/0.94
HTML::Template::Pro/0.92
Template/2.20
Text::MicroTemplate/0.10
Benchmark: running HT, HT::C, HT::Pro, MT, TT for at least 1 CPU seconds...
HT: 1 wallclock secs ( 1.03 usr + 0.00 sys = 1.03 CPU) @ 1094.17/s (n=1127)
HT::C: 1 wallclock secs ( 1.03 usr + 0.00 sys = 1.03 CPU) @ 11598.06/s (n=11946)
HT::Pro: 2 wallclock secs ( 0.82 usr + 0.29 sys = 1.11 CPU) @ 17611.71/s (n=19549)
MT: 1 wallclock secs ( 0.96 usr + 0.08 sys = 1.04 CPU) @ 15904.81/s (n=16541)
TT: 1 wallclock secs ( 1.08 usr + 0.00 sys = 1.08 CPU) @ 7110.19/s (n=7679)
Rate HT TT HT::C MT HT::Pro
HT 1094/s -- -85% -91% -93% -94%
TT 7110/s 550% -- -39% -55% -60%
HT::C 11598/s 960% 63% -- -27% -34%
MT 15905/s 1354% 124% 37% -- -10%
HT::Pro 17612/s 1510% 148% 52% 11% --
何回かやったのですが、自分の環境ではまだHTML::Template::Proが速いみたい。でも僅差なのでまったく気にならないレベルですね。とにかくgfxさん++すぎる。
コメント