Perlで、サブルーチンを入れ子にして、レキシカル変数を外と内で共有するということを考えてみた。
use strict;
use warnings;
sub test1 {
my $count = 0;
print $count, "\n";
sub test2 {
print '->', ++$count;
$count < 5 ? test2() : print "\n";
}
test2();
print $count, "\n\n";
}
test1();
# Variable "$count" will not stay shared at c.pl line 9.
# 0
# ->1->2->3->4->5
# 5
実行はできているが、Variable "$count" will not stay shared のメッセージが出ている。
メッセージの意味はperldiagに記載されている。日本語訳perldiagより引用。
- Variable "%s" will not stay shared
(W closure) 内部の(ネストした) 名前付き サブルーチンが、 外側のサブルーチンで定義したレキシカル変数を参照しています。
内側のサブルーチンが呼び出された時、おそらく外側のサブルーチンの値は、 最初の外側のサブルーチンへの呼び出し前および呼び出し中のものになります; この場合、外側のサブルーチンへの最初の呼び出しが終了した後、内側と 外側のサブルーチンは変数に関して同じ値を共有しなくなります。 言い換えると、変数はもはや共有されません。
さらに、外側のサブルーチンが無名で、それ自身の外側のレキシカル変数を 参照している場合、外側と内側のサブルーチンは与えられた変数は 共有 しません。
この問題は普通、
sub {}
構文を使って内側のサブルーチンを無名にすることで 解決します。 外側のサブルーチンの変数を参照している内側の無名サブルーチンが 呼び出されたり参照されたとき、これらはそのような変数の現在の値に 自動的に回復します。
共有しない、というところで少々ひっかかった。実行結果からすると、test2()
の中で変更された$countの値はtest2()
が終わった後も残っており、値が共有されているように見えるので。
しかしtest1()
を複数回実行してみると、以下のような結果となる。
# 省略
test1();
test1();
test1();
Variable "$count" will not stay shared at c.pl line 9. 0 ->1->2->3->4->5 5 0 ->6 0 0 ->7 0
test1()
の直下では毎回値が0に設定されいてるのに対し、test2()
の中では前に実行した時の値が残り続けている。確かに共有されていない。
なぜこういう動作になるのかと考えてみて、「Perlにおいては sub name {}
の宣言はどこで書いてもグローバルになる」ということに気付くと納得できた。実際、test2()
はtest1()
の外でも実行可能であった。
# 省略
test1();
test2();
Variable "$count" will not stay shared at c.pl line 9. 0 ->1->2->3->4->5 5 ->6
test2()
はtest1()
実行のたびに作成されるのではなく、1度宣言したらそのまま残り続けている。そのため、その外で宣言されたレキシカル変数$countへの参照も、最初に宣言された時点のものが保持され続ける……ということだと思う。
警告メッセージを出さないようにするには、perldiagにあるとおり、内側のサブルーチンを無名サブルーチンにすればいい。
use strict;
use warnings;
sub test1 {
my $count = 0;
print $count, "\n";
my $test2;
$test2 = sub {
print '->', ++$count;
$count < 5 ? $test2->() : print "\n";
};
$test2->();
print $count, "\n\n";
}
test1();
test1();
# 0
# ->1->2->3->4->5
# 5
#
# 0
# ->1->2->3->4->5
# 5
ちなみにここで my $test2 = sub { ... }
と書くと、内側のサブルーチンの中で 'Global symbol "$test2" requires explicit package name' というエラーになった。
このようなことを考えたきっかけは、再帰?は難しい - 刺身☆ブーメランのはてなダイアリーの記事を読み、「@linksを外で宣言するのではなく、さらに外側をsub
で囲えばいいんじゃないか」と思ったことからでした。一応その考えをもとにブックマークファイルを読み込むスクリプトを自分なりに書いてみたところ、以下のようになりました。
use strict;
use warnings;
use Netscape::Bookmarks;
use Data::Dumper;
$Data::Dumper::Indent = 1;
sub extract_bookmark {
my ($file) = @_;
my @categories;
my @links;
my $_parse_recursively;
$_parse_recursively = sub {
my ($category) = @_;
ref $category or $category = Netscape::Bookmarks->new($category);
for my $element (@{$category->elements}) {
if ($element->isa('Netscape::Bookmarks::Category')) {
push @categories, $element->title;
$_parse_recursively->($element);
}
elsif ($element->isa('Netscape::Bookmarks::Link')) {
push @links, {
bookmark => $element,
categories => [ @categories ],
};
}
}
pop @categories;
};
$_parse_recursively->($file);
return \@links;
}
my $links_ref = extract_bookmark('bookmarks.html');
print Dumper $links_ref;
ところでJavaScriptだと、関数の中に書かれた関数宣言は外側の関数内ローカルなものとなります。最近自分用のgresemonkeyのスクリプトを書き直したりしていたので、以上のようなことを考えたのもそのことが頭にあったからだと思います。
function a() {
alert('hello!');
function b() {
alert('goodbye!');
}
b(); // goodbye!
}
a(); // hello!
b(); // (b is not defined)