camel

いい点に気づかれました。

perl の配列とメモリー: 国民宿舎はらぺこ 大浴場
面白いな、と思ったのは、上記リンク先の話題を手元で試していたときに、
@data = map { rand 10 } (1..1e7);
$sum += $_ for @data;
だとメモリーを喰いまくるのに、
$sum += rand 10 for 1..1e7;
だとほとんどメモリーを喰わないこと。

実は、foreach($from..$to)は、Perl 5.005以来最適化されています。

perl5005delta - what's new for perl5.005 - search.cpan.org
foreach (1..1000000) optimized foreach (1..1000000) is now optimized into a counting loop. It does not try to allocate a 1000000-size list anymore.

さすがにこれだけ前のversionだと、日本語訳もあります。

ただし残念なことに、これは foreach のみの特権で、他の場合は相変わらず巨大配列を作ってしまいます。

Perl 6では、遅延評価、すなわち必要な時にだけ要素が生成されるということがサポートされているので、Haskellのように無限リストも扱えるようになる予定です。

が、同様のことはPerl 5でも実は簡単に出来てしまいます。

sub make_range {
    my ( $now, $end ) = @_;
    return sub { $now <= $end ? $now++ : undef }
}
my $sum = 0;
my $r = make_range(1, 1e7);
$sum += $_ while(defined($_ = $r->())); # definedでくくらないと、0で抜けてしまう
print "$sum\n"

ただし、Nativeでないので特別扱いされたforよりもかなり遅いのですが。

% /usr/bin/time perl make_range.pl
50000005000000
        9.89 real         9.81 user         0.03 sys
% /usr/bin/time perl -le '$sum+=$_ for(1..1e7);print $sum'
50000005000000
        2.12 real         2.10 user         0.00 sys

その代わり、このやり方はずっと柔軟です。例えば、逆順も行けます。

sub make_range {
    my ( $now, $end, $step ) = @_;
    sub { abs($end - $now) >= 1 ? $now += $step : undef }
}

my $sum = 0;
my $r = make_range(100, 0, -2);
$\="\n";
print  while(defined($_ = $r->()));

こういったものを、IteratorとかGeneratorとかと呼びます。見ての通り、ClosureがあればPerlに限らず簡単に実装できるのです。

しかし、Tieという仕組みでこれを全く見えなくできてしまうのはPerlならではの醍醐味でもあります。例えば、こんなことも。

このように、IteratorとしてだけArrayを使うのであれば、上記の3つのメソッドを定義するだけでOKです。

{
    package Tie::Iterator::Simple;
    sub TIEARRAY{
        my ($pkg, $start, $end) = @_;
        bless { start => $start, end => $end }, $pkg;
    }
    sub FETCH{
        my ($this, $index) = @_;
        $this->{start} + $index;
    }
    sub FETCHSIZE{
        my ($this, $index) = @_;
        $this->{end} - $this->{start} + 1;
    }
}

tie my @iter, 'Tie::Iterator::Simple', 1, 1e6; # 一桁少ない数で実験
my $sum = 0;
$sum += $_ for(@iter);
print "$sum\n";

もっとも、スピードは早くありませんが。

% /usr/bin/time perl tie_iter.pl
500000500000        7.58 real         7.47 user         0.03 sys
% /usr/bin/time perl -le '$sum+=$_ for(1..1e6);print $sum'
500000500000
        0.21 real         0.20 user         0.00 sys

Perlは他の言語からさまざまな機能を盗んでいますが、このTieは他に見当たらないユニークな特徴と言えるでしょう。

Dan the Tied to Perl

追記:

www.textfile.org - perl - for(1..1e10) と Iterator
ところで細かい話ですが、
bless { start => $start, end => $end }
のところは、
bless { start => $start, end => $end }, $pkg;
のほうがよい? まあ、サブクラス化しなければよいのか。

直しました。まあ上記のような「使い捨て」クラスの場合はこれでもよさげですが、better practiceということで。いつもありがとうございます。