subの中にsub

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より引用。

共有しない、というところで少々ひっかかった。実行結果からすると、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)