Template Toolkitでwantarrayなものを使おうとしてハマった件

wantarrayで返り値を切り替えるようなものをTemplate Toolkitで使おうとしてハマった件。

ハマった例

#!/usr/bin/env perl
package Hoge;

sub new {
    my ($class, $arg) = @_;
    bless +{ data => +[] }, $class;
}

sub fuga {
    my ($self) = @_;
    return wantarray ? @{ $self->{data} } : $self->{data};
}

package main;
use strict;
use warnings;

use Template;

# hoge1 has only 1 element
my $hoge1 = Hoge->new();
push @{ $hoge1->{data} }, +{ foo => 'bar' };
warn $hoge1->fuga->[0]->{foo};
warn +($hoge1->fuga)[0]->{foo};

# hoge2 has 2 elements
my $hoge2 = Hoge->new();
push @{ $hoge2->{data} }, +{ foo => 'bar' };
push @{ $hoge2->{data} }, +{ foo => 'baz' };
warn $hoge2->fuga->[0]->{foo};
warn +($hoge2->fuga)[0]->{foo};

my $template = do {
    local $/ = undef;
    <DATA>
};
Template->new->process(\$template, +{ i => 1, hoge => $hoge1 });
Template->new->process(\$template, +{ i => 2, hoge => $hoge2 });

__END__
[% piyo = hoge.fuga.list.0 %]
case [% i %]:
piyo      : [% piyo %]
piyo.foo  : [% piyo.foo %]
piyo.keys : [% piyo.keys().join(',') %]

Hoge::fugaはwantarrayでコンテキストによって返すものが変わる、というやつ。そしてhoge1にはハッシュリファレンスを1つだけ挿入、hoge2には2つ挿入してみる。そして返ってくるモノから0番目の要素を取り出して表示しようとする。
これを実行すると

bar at tt.pl line 23.
bar at tt.pl line 24.
bar at tt.pl line 30.
bar at tt.pl line 31.

case 1:
piyo      : HASH(0x7fc0cb1423b8)
piyo.foo  : 
piyo.keys : value,key

case 2:
piyo      : HASH(0x7fc0cb027630)
piyo.foo  : bar
piyo.keys : foo

となる。hoge1もhoge2でもPerl文法上で0番目の要素のfooを取り出すのに何も問題ないけど、Template内で取り出そうとすると要素が1つだけのhoge1の場合におかしなことになる。本来、0番目のものを取り出すだけなのだから要素数は関係ないはずなのに。

回避策

で、軽くググってみると、やっぱり要素数が1つだけのときのwantarrayに対する挙動で問題があったらしい。

wantarrayは避けて常にリファレンスを受け取るようにしましょう、というような。
コード側を変えられない場合、Template-Toolkit 2.20以降であればScalar Pluginがあるのでそれを使うことで明示的にscalar contextで受け取ることが出来るようだ。
Template::Plugin::Scalar - search.cpan.org

以下のようにtemplateを変更すると、要素数に関係なく ちゃんと0番目の要素を正しく取得できる。

[% USE scalar %][% piyo = hoge.scalar.fuga.list.0 %]
case [% i %]:
piyo      : [% piyo %]
piyo.foo  : [% piyo.foo %]
piyo.keys : [% piyo.keys().join(',') %]

実行結果:

bar at tt.pl line 23.
bar at tt.pl line 24.
bar at tt.pl line 30.
bar at tt.pl line 31.

case 1:
piyo      : HASH(0x7fceea026d18)
piyo.foo  : bar
piyo.keys : foo

case 2:
piyo      : HASH(0x7fceea027630)
piyo.foo  : bar
piyo.keys : foo

Text::Xslateでは?

Template Toolkitと同様のsyntaxを使えるText::Xslateの場合はどうなるのだろう?と思って試してみた。

#!/usr/bin/env perl
package Hoge;

sub new {
    my ($class, $arg) = @_;
    bless +{ data => +[] }, $class;
}

sub fuga {
    my ($self) = @_;
    return wantarray ? @{ $self->{data} } : $self->{data};
}

package main;
use strict;
use warnings;

use Text::Xslate;

# hoge1 has only 1 element
my $hoge1 = Hoge->new();
push @{ $hoge1->{data} }, +{ foo => 'bar' };
warn $hoge1->fuga->[0]->{foo};
warn +($hoge1->fuga)[0]->{foo};

# hoge2 has 2 elements
my $hoge2 = Hoge->new();
push @{ $hoge2->{data} }, +{ foo => 'bar' };
push @{ $hoge2->{data} }, +{ foo => 'baz' };
warn $hoge2->fuga->[0]->{foo};
warn +($hoge2->fuga)[0]->{foo};

my $template = do {
    local $/ = undef;
    <DATA>
};
my $tx = Text::Xslate->new(syntax => 'TTerse');
print $tx->render_string($template, +{ i => 1, hoge => $hoge1 });
print $tx->render_string($template, +{ i => 2, hoge => $hoge2 });

__END__
[% piyo = hoge.fuga.0 %]
case [% i %]:
piyo      : [% piyo %]
piyo.foo  : [% piyo.foo %]
piyo.keys : [% piyo.keys().join(',') %]

こちらは特にそういった問題は起きないようだ。

bar at tx.pl line 23.
bar at tx.pl line 24.
bar at tx.pl line 30.
bar at tx.pl line 31.


case 1:
piyo      : HASH(0x7fe7e4026ce8)
piyo.foo  : bar
piyo.keys : foo


case 2:
piyo      : HASH(0x7fe7e4027600)
piyo.foo  : bar
piyo.keys : foo