Perl は ver 5 で「リファレンス」と「オブジェクト指向」が追加されましたが、 このほかにも Lisp から便利な機能をいくつか取り込んでいます。「無名関数」もそのひとつですが、もうひとつ Lisp から取り込んだ機能に「クロージャ (closure)」があります。無名関数やクロージャが Perl でも使うことができるとは、Lisp 好きな M.Hiroi も大変驚いたものです。今回はクロージャについて簡単に説明しましょう。
クロージャを一言で説明すると、「実行する関数とアクセス可能な局所変数をまとめたもの」ということになります。クロージャは関数のように実行することができますが、クロージャを生成するときに有効な局所変数を保持するところが異なります。それでは、具体的に説明していきましょう。
Perl の場合、クロージャを生成するには「無名関数」を使います。次のリストを見てください。関数 foo は何をするのでしょうか。
リスト : クロージャの生成 (sample0901.pl) use strict; use warnings; sub foo { my $x = shift; sub { my $y = shift; $x * $y; } } my $func1 = foo(10); my $func2 = foo(100); print $func1->(4), "\n"; print $func1->(5), "\n"; print $func2->(1), "\n"; print $func2->(2), "\n";
foo は無名関数を作成し、それを返していますね。したがって、foo の返り値を変数にセットして、それを実行することができます。
それでは、実行してみましょう。結果は次のようになります。
$ perl sample0901.pl 40 50 100 200
変数 $func1 に格納された関数は、引数を 10 倍した値を返しています。そして、変数 $func2 に格納された関数は引数を 100 倍していますね。つまり、関数 foo は引数を定数倍する関数を生成していたのです。
ここで、もう一度 foo のプログラムを見てください。無名関数の中で、変数 $x にアクセスしていることに注目しください。この変数 $x は無名関数の中では定義されていません。では、無名関数が実行されるとき、$x はどの変数にアクセスするのでしょう。大域変数でしょうか。いいえ、そうではありません。関数 foo で定義された局所変数 $x にアクセスするのです。
無名関数を生成するとき、実行する関数のほかに、そのときに有効な局所変数もいっしょに保存されます。これを「環境 (environment)」と呼びます。この場合、無名関数が生成されるときに有効な局所変数は $x です。したがって、この変数 $x とその値がクロージャに保存されます。
クロージャを実行するときは、取り出した関数を保存してある「環境」で実行します。ここがクロージャを理解するポイントです。foo(10) を実行して無名関数を生成するとき、有効な局所変数は $x で、その値は 10 ですね。これがクロージャに保存されているので、$func1 の関数は引数を 10 倍した結果を返します。foo(100) を実行すると $x の値は 100 で、それがクロージャに保存されているので、$func2 の関数は引数を 100 倍した結果を返すのです。
Perl でクロージャを使う場合、注意事項が一つあります。局所変数は my と local で定義できますが、クロージャ内に保存されるのは my で定義された変数だけです。local で定義された変数は保存されないことに注意してください。これは、my と local では変数の有効範囲が異なるためです。変数の有効範囲を表すのに「スコープ」という用語があります。この用語を使うと my が「レキシカルスコープ」で、local が「ダイナミックスコープ」となります。次のプログラムを見てください。
リスト : my と local の違い (sample0902.pl) use strict; use warnings; our $x = 10; our $y = "one"; sub bar { print "$x\n$y\n"; } sub foo { my $x = 100; local $y = "two"; bar(); } foo;
まず、大域変数 $x と $y を定義します。関数 foo では、局所変数 $x を my で定義し、$y を local で定義して関数 bar を呼び出します。関数 bar で局所変数は定義していません。さて、bar で表示される $x と $y の値はどうなるでしょうか。実行結果は次のようになります。
$ perl sample0902.pl 10 two
$x は大域変数の値 10 で、$y は foo で定義した値 two になりました。foo で定義した局所変数 $x は、関数 bar からアクセスすることはできません。そのため、大域変数の値が表示されました。これが my で定義した局所変数の特徴です。これを「レキシカルスコープ」といいます。
レキシカルとは文脈上という意味があり、変数が定義されている位置によって、その変数にアクセスできるかどうかが決まります。この場合、関数 bar 内で変数 $x が定義されていないので、局所変数ではなく大域変数として扱われるのです。
逆に、local で定義した変数は、呼び出し先の関数からでもアクセスすることができます。関数 bar で two が表示されたのは、呼び出し元の関数 foo で局所変数 $y が local で定義されていたからです。これを「ダイナミックスコープ」といいます。foo で定義された変数 $y は、foo の実行が終了するまで存在し、foo から呼び出された関数ならば、どこからでもアクセスすることができます。
ダイナミックスコープの場合、関数の実行順序や局所変数の定義によって、アクセスする変数が左右されることに注意してください。次のリストを見てください。
リスト : ダイナミックスコープ (sample0903.pl) use strict; use warnings; our $x = 10; our $y = "one"; sub bar { print "$x\n$y\n"; } sub foo2 { local $y = "three"; bar(); } sub foo1 { local $x = 1000; bar(); foo2(); } foo1;
$ perl sample0903.pl 1000 one 1000 three
関数 foo1 から bar を呼び出す場合、$x は foo1 で定義した値 1000 になり、$y は foo1 で定義されていないので大域変数の値 one となります。次に、foo1 から foo2 を呼び出し、foo2 から bar を呼び出す場合、foo2 では $x を定義していないので、bar の $x は foo1 で定義した値 1000 になり、$y は foo2 で定義した値 three となります。
このように、ダイナミックスコープでは同じ関数 bar でも、実行する順番によって表示される値が異なります。このため、プログラムを一見しただけでは、どの変数にアクセスするのかわかりません。レキシカルスコープであれば、プログラムを見ただけでアクセスする変数を特定することができます。たとえば関数 bar の場合、$x と $y が大域変数であることは明らかですね。プログラムの読みやすさを考えると、レキシカルスコープの方がわかりやすいと思います。
Lisp の場合、最初にレキシカルスコープを採用した処理系が Scheme で、そのあと Common Lisp に取り入れられました。それ以前の Lisp はダイナミックスコープです。ちなみに、Emacs Lisp はダイナミックスコープで、Common Lisp に準拠している xyzzy Lisp はレキシカルスコープです。Lisp のスコープについて興味をお持ちの方は、拙作のページ Common Lisp 入門「レキシカルスコープとクロージャ」をお読みください。
クロージャの応用例として「ジェネレータ (generator)」というプログラムを紹介しましょう。ジェネレータは、呼び出されるたびに新しい値を生成していきます。
簡単な例題として、奇数列 (1, 3, 5, ...) を発生するジェネレータ gen_odd_number を作りましょう。gen_odd_number は呼び出されるたびに新しい奇数を返します。いちばん簡単な実装方法は、返した値を大域変数に記憶しておくことです。gen_odd_number のプログラムは次のようになります。
リスト : 奇数を発生するジェネレータ (sample0904.pl) use strict; use warnings; our $prev_number = -1; sub get_odd_number { $prev_number += 2; } foreach (1 .. 10) { print get_odd_number, " "; } print "\n";
$ perl sample0904.pl 1 3 5 7 9 11 13 15 17 19
大域変数 $prev_number は gen_odd_number が返した値を記憶します。新しい値は、この $prev_number に 2 を足せばいいのです。
このように、大域変数を使うと簡単にジェネレータを作ることができますが問題点もあります。それは、複数のジェネレータが必要になる場合です。単純に考えると、必要な数だけ大域変数と関数を用意すればいいのですが、数が増えると大域変数や関数を定義するだけでも大変な作業になります。
ところがクロージャを使うと、もっとスマートにジェネレータを用意できます。まず、ジェネレータを作る関数を定義します。
リスト : ジェネレータを作る関数 (sample0905.pl) use strict; use warnings; sub make_gen { my $prev_number = -1; sub { $prev_number += 2; } } my $g1 = make_gen; foreach (1 .. 10) { print $g1->(), " "; } print "\n"; my $g2 = make_gen; foreach (1 .. 10) { print $g2->(), " "; } print "\n";
関数 make_gen はクロージャを返します。そして、このクロージャがジェネレータの役割を果たすのです。それでは、実際に実行してみましょう。
$ perl sample0905.pl 1 3 5 7 9 11 13 15 17 19 1 3 5 7 9 11 13 15 17 19
make_gen で作成したクロージャを変数 $g1 にセットして実行します。実行するたびに 1, 3, 5 と奇数列を生成していますね。次に新しいクロージャを変数 $g2 にセットします。このクロージャを実行すると、新しい奇数列を生成します。確かにジェネレータとして動作しています。
このプログラムのポイントは局所変数 $prev_number です。クロージャに保存されている局所変数は $prev_number です。この値は make_gen が実行されたときに -1 で初期化されています。クロージャにはこの値が保存されています。
次に、$g1 にセットしたクロージャを実行します。無名関数はクロージャに保存された局所変数にアクセスするので、$prevNumber += 2 の値は 1 になり、クロージャに保持されている $prev_number の値は 1 に更新されます。
環境はクロージャによって異なります。$g1 のクロージャが実行されると、そのクロージャの環境が更新されるのであって、ほかのクロージャに影響を与えることはありません。したがって、ジェネレータが発生する奇数列が、ほかのジェネレータに影響を与えることはないのです。あとは必要な数だけジェネレータを make_gen で作り、そのクロージャを変数に格納しておけばいいわけです。
次は、奇数列を最初に戻す、つまり、ジェネレータをリセットすることを考えてみましょう。この場合、クロージャ内の変数を書き換えるしか方法はありません。そこで、make_gen の返り値を 2 つに増やすことにします。最初の返り値は奇数列を発生するジェネレータで、2 番目の返り値はジェネレータをリセットする関数とします。プログラムは次のようになります。
リスト : ジェネレータのリセット (sample0906.pl) use strict; use warnings; sub make_gen { my $prev_number = -1; my $gen = sub { $prev_number += 2; }; my $reset = sub { $prev_number = -1; }; ($gen, $reset); } my ($g1, $r1) = make_gen; foreach (1 .. 10) { print $g1->(), " "; } print "\n"; $r1->(); foreach (1 .. 10) { print $g1->(), " "; } print "\n";
make_gen の変数 gen に奇数列を生成する関数をセットし、変数 reset に $prev_number を -1 に書き換える関数をセットします。あとは、gen と reset をリストに格納して返すだけです。make_gen を呼び出す側はリストコンテキストで 2 つの関数を受け取ります。g1 を呼び出すと奇数列を生成し、r1 を呼び出すとジェネレータはリセットされます。
それでは実行してみましょう。
$ perl sample0906.pl 1 3 5 7 9 11 13 15 17 19 1 3 5 7 9 11 13 15 17 19
正常に動作していますね。
次は、GUI でクロージャを使ってみましょう。Tk などの GUI プログラミングでは、ある操作を行ったときに特定の関数を呼び出す仕組みが用意されています。たとえば Tk では、ボタンを押したときに実行する関数をあらかじめ登録しておくことができます。この関数を「コールバック関数」といいます。簡単な例として、押したボタンの番号を表示するプログラムを Perl/Tk で作ってみましょう。
リスト : ボタンとラベルを表示する (sample0907.pl) use strict; use warnings; use Tk; # グローバル変数 our $buffer = ""; # メインウィンドウの生成 my $top = MainWindow->new(); # ラベルの作成 my $l = $top->Label(-textvariable => \$buffer); $l->pack(); # ボタンを押した時の動作 sub push_button { my $n = shift; $buffer = "button $n pressed"; } # ボタンの生成 foreach my $n (1, 2, 3, 4) { $top->Button(-text => "button $n", -command => [\&push_button, $n])->pack(-fill => 'x'); } MainLoop();
関数 push_button がコールバック関数です。Perl/Tk の場合、ボタンのオプション -command でコールバック関数を登録します。指定方法は「関数へのリファレンス」または「無名の配列」を使います。無名の配列の場合、コールバック関数に引数を渡すことができます。このプログラムでは、ボタンの番号 $n を引数として渡していますが、配列を生成する時に、$n は数値に変換されることに注意してください。
ボタンを 4 つ生成し、そのボタンを押すと関数 push_button が呼び出され、上部のラベルに押したボタンの番号が表示されます。Perl/Tk の詳しい説明は、拙作のページ「Perl/Tk Memo」をお読みください。
それでは、ボタンの番号だけではなく、ボタンを押した回数も表示するようにプログラムを改造してみましょう。この処理は大域変数を使って実現することもできますが、クロージャを使った方がスマートです。プログラムは次のようになります。
リスト : コールバック関数にクロージャを使用 (sample0908.pl) use strict; use warnings; use Tk; # グローバル変数 our $buffer = ""; # メインウィンドウの生成 my $top = MainWindow->new(); # ラベルの作成 my $l = $top->Label(-textvariable => \$buffer); $l->pack(); # クロージャでボタンを生成 sub make_button { my $n = shift; my $c = 0; $top->Button(-text => "button $n", -command => sub { $c++; $buffer = "button $n pressed $c times"; })->pack(-fill => 'x'); } # ボタンの生成 foreach my $n (1, 2, 3, 4) { make_button($n); } MainLoop();
関数 make_button でボタンを生成します。変数 $n がボタンの番号で、ボタンを押した回数は変数 $c でカウントします。そして、オプション -command にクロージャをセットします。ボタンが押されるとクロージャが実行され、変数 $c を +1 してからボタンの番号と押した回数を表示します。このように、クロージャを使うことで、ちょっと複雑なコールバック関数でもスマートに実現することができます。
関数型言語では高階関数とクロージャを組み合わせることで簡単にプログラムを作ることができるようになっています。簡単な例として、配列を線形探索する関数 find, position, count の高階関数バージョン find_if, position_if, count_if を作ってみましょう。find_if($f, $xs) は配列 $xs から関数 $f が真を返す要素を探します。position_if($f, $xs) は $f が真を返す要素の位置を返します。count_if($f, $xs) は $f が真を返す要素の個数を返します。
プログラムは次のようになります。。
リスト : 線形探索 (高階関数版, sample0909.pl) use strict; use warnings; sub find_if { my ($f, $xs) = @_; foreach my $x (@$xs) { return 1 if $f->($x); } 0; } sub position_if { my ($f, $xs) = @_; for (my $i = 0; $i < @$xs; $i++) { return $i if $f->($xs->[$i]); } -1; } sub count_if { my ($f, $xs) = @_; my $c = 0; foreach my $x (@$xs) { $c++ if $f->($x); } $c; }
どの関数も引数の関数 $f に配列の要素を渡して呼び出し、返り値が真であれば所定の動作を行います。find_if であれば真 (1) を返し、position_if であれば添字 $i を返し、count_if であれば変数 $c の値をインクリメントします。あとは特に難しいところはないでしょう。
それでは実際に動かしてみましょう。
リスト : 簡単なテスト my @a = (1 .. 8); foreach my $i (0 .. 9) { print find_if(sub {shift == $i}, \@a), " "; } print "\n"; foreach my $i (0 .. 9) { print position_if(sub {shift == $i}, \@a), " "; } print "\n"; print count_if(sub {my $y = shift; $y % 2 == 0}, \@a), "\n"; print count_if(sub {my $y = shift; $y % 3 == 0}, \@a), "\n";
$ perl sample0909.pl 0 1 1 1 1 1 1 1 1 0 -1 0 1 2 3 4 5 6 7 -1 4 2
find_if と position_if に渡す無名関数はクロージャなので、foreach で宣言されている局所変数 $i にアクセスすることができます。配列の要素は数値なので演算子 == を使いましたが、配列の要素が文字列であれば、無名関数の中で使用する演算子を eq に変えるだけで動作します。また、count_if のように高階関数に渡す処理を変更することで、偶数の個数や 3 で割り切れる要素の個数など、いろいろな処理に対応させることができます。
次は、関数型言語でよく使われる便利な高階関数 (マッピング、フィルター、畳み込み) を紹介します。なお、関数に引数を与えて呼び出すことを、関数型言語の世界では「適用」といいます。本稿でも関数呼び出しの意味で適用を使うことにします。
まず最初に、配列の要素に引数の関数 f を適用し、その結果を配列に格納して返す関数を作ります。このような操作を「マッピング (写像)」といいます。次のリストを見てください。
リスト : マッピング sub mapcar { my ($f, $xs) = @_; my $ys = []; foreach my $x (@$xs) { push @$ys, $f->($x); } $ys; }
関数名は mapcar としました。名前は Common Lisp から拝借しました。プログラムは簡単です。foreach 文で配列 $xs の要素を順番に取り出し、それを関数 $f に渡して評価します。その結果を配列 $ys に追加し、最後に $ys を返します。
フィルター (filter) は配列の要素に関数を適用し、関数が真を返す要素を配列に格納して返す関数です。プログラムは次のようになります。
リスト : フィルター sub filter { my ($f, $xs) = @_; my $ys = []; foreach my $x (@$xs) { push @$ys, $x if $f->($x); } $ys; }
mapcar と同様に filter も簡単です。最初に空の配列を変数 $ys にセットします。そして関数 $f が真を返したならば $x を push で配列 $ys に追加するだけです。
2 つの引数を取る関数 f と配列を引数に受け取る関数 reduce を考えます。reduce は配列の各要素に対して関数 f を下図のように適用します。
(1) [a1, a2, a3, a4, a5] => f( f( f( f( a1, a2 ), a3 ), a4 ), a5 ) (2) [a1, a2, a3, a4, a5] => f( a1, f( a2, f( a3, f( a4, a5 ) ) ) ) 図 : reduce の動作
関数 f を適用する順番で 2 通りの方法があります。図 (1) は配列の先頭から f を適用し、図 (2) は配列の後ろから f を適用します。たとえば、関数 f が単純な加算関数とすると、reduce の結果はどちらの場合も配列の要素の和になります。
f(x, y) = x + y の場合 reduce => a1 + a2 + a3 + a4 + a5
このように、reduce は配列のすべての要素を関数 f を用いて結合します。このような操作を「縮約」とか「畳み込み」といいます。また、reduce の引数に初期値 g を指定することがあります。この場合、reduce は下図に示す動作になります。
(1) [a1, a2, a3, a4, a5] => f( f( f( f( f( g, a1 ), a2 ), a3 ), a4 ), a5 ) (2) [a1, a2, a3, a4, a5] => f( a1, f( a2, f( a3, f( a4, f( a5, g ) ) ) ) ) 図 : reduce() の動作 (2)
ここでは簡単な例題として、上図 (1) の動作を行う関数 fold_left と、上図 (2) の動作を行う関数 fold_right を作ってみましょう。次のリストを見てください。
リスト : 畳み込み sub fold_left { my ($f, $a, $xs) = @_; foreach my $x (@$xs) { $a = $f->($a, $x); } $a; } sub fold_right { my ($f, $a, $xs) = @_; for (my $i = @$xs - 1; $i >= 0; $i--) { $a = $f->($xs->[$i], $a); } $a; }
fold_left の引数 $xs が配列、$a が初期値です。foreach で $xs の要素を一つずつ取り出して関数 $f に渡して実行します。その結果を変数 a にセットします。fold_left は変数 a の値を関数 f の返り値で更新することで、図 (1) の動作を実現しています。
たとえば、配列が [1, 2, 3] で a が 0 とします。最初は f(0, 1) が実行され、その返り値が a にセットされます。次は f(a, 2) が実行されますが、これは f(f(0, 1), 2) と同じことです。そして、その結果が a にセットされます。最後に f(a, 3) が実行されますが、これは f(f(f(0, 1), 2), 3) となり、図 (1) と同じ動作になります。
fold_left の場合、配列の要素が関数の第 2 引数になり、第 1 引数にはこれまでの処理結果が渡されます。これに対し、fold_right の場合は逆になり、関数 $f の第 1 引数に配列の要素が渡されて、これまでの処理結果は第 2 引数に渡されます。
fold_left, fold_right は関数型言語でよく用いられる高階関数で、2 引数の関数と組み合わせると、いろいろな処理を簡単に実現することができます。
それでは、簡単な実行例を示します。
リスト : 高階関数の使用例 (sample0910.pl) use strict; use warnings; # # 今までの関数定義は省略 # # 簡単なテスト my @a = (1 .. 10); my $b = mapcar(sub {my $x = shift; $x * $x}, \@a); my $c = mapcar(sub {my $x = shift; $x * $x * $x}, \@a); print "@$b\n@$c\n"; $b = filter(sub {$_[0] % 2 == 0}, \@a); $c = filter(sub {$_[0] % 2 != 0}, \@a); print "@$b\n@$c\n"; print fold_left(sub {$_[0] + $_[1]}, 0, \@a), "\n"; print fold_right(sub {$_[0] + $_[1]}, 0, \@a), "\n";
$ perl sample0910.pl 1 4 9 16 25 36 49 64 81 100 1 8 27 64 125 216 343 512 729 1000 2 4 6 8 10 1 3 5 7 9 55 55
今度は filter を使って、指定した文字 $code が先頭にある文字列を削除する関数 remove_string を作ってみましょう。最初に、簡単な動作例を示します。
my @a = ("abc", "def", "abc", "ghi"); remove_string('a', \@a); => [def, ghi] remove_string('d', \@a); => [abc, abc, ghi]
remove_string は先頭文字と $code を比較しなければならないので、単純に考えると filter に渡す関数には 2 つの引数が必要なはずです。しかし、filter が関数に渡すのは配列の要素だけです。これでプログラムできるのか疑問に思われた方もいるでしょう。ところが、クロージャを使うとうまくいくのです。次のプログラムを見てください。
リスト : 先頭文字が $code の文字列を削除 sub remove_string { my ($code, $xs) = @_; filter(sub {substr(shift, 0, 1) eq $code ? 0 : 1;}, $xs); }
先頭文字は組み込み関数 substr で取り出します。substr は文字列から部分列を切り出す関数です。
substr 文字列, 開始, 終点
substr(shift, 0, 1) で引数の先頭文字を取り出すことができます。
remove_string の無名関数に注目してください。クロージャの働きにより、無名関数の中で remove_string の局所変数 $code にアクセスすることができるので、先頭文字と $code を比較することができるのです。このように、高階関数とクロージャを組み合わせることで、とても簡単にプログラムを作ることができます。
今回はここまでです。クロージャは少し難しいかもしれませんが、便利で面白い機能です。少々歯応えがありますが、これもプログラミングの面白いところだと思います。Perl は手続き型言語なのでクロージャを使う機会は少ないと思いますが、興味のある方はいろいろと試してみてください。
また、関数を扱うことは、やっぱり関数型言語の方が優れています。クロージャの話に興味をもたれた方は、ぜひ Lisp / Scheme や関数型言語 (SML/NJ, OCaml, Haskell など) にも挑戦してみてください。
リスト : 高階関数 (sample0911.pl) use strict; use warnings; # マッピング sub mapcar { my ($f, $xs) = @_; my $ys = []; foreach my $x (@$xs) { push @$ys, $f->($x); } $ys; } # フィルター sub filter { my ($f, $xs) = @_; my $ys = []; foreach my $x (@$xs) { push @$ys, $x if $f->($x); } $ys; } # 畳み込み sub fold_left { my ($f, $a, $xs) = @_; for my $x (@$xs) { $a = $f->($a, $x); } $a; } sub fold_right { my ($f, $a, $xs) = @_; for (my $i = @$xs - 1; $i >= 0; $i--) { $a = $f->($xs->[$i], $a); } $a; } # 先頭文字が $code の文字列を削除 sub remove_string { my ($code, $xs) = @_; filter(sub { substr(shift, 0, 1) eq $code ? 0 : 1; }, $xs); } my @a = (1 .. 10); my $b = mapcar(sub {my $x = shift; $x * $x}, \@a); my $c = mapcar(sub {my $x = shift; $x * $x * $x}, \@a); print "@$b\n@$c\n"; $b = filter(sub {$_[0] % 2 == 0}, \@a); $c = filter(sub {$_[0] % 2 != 0}, \@a); print "@$b\n@$c\n"; print fold_left(sub {$_[0] + $_[1]}, 0, \@a), "\n"; print fold_right(sub {$_[0] + $_[1]}, 0, \@a), "\n"; my @d = ("abc", "def", "abc", "ghi"); my $e = remove_string('a', \@d); print "@$e\n"; $e = remove_string('d', \@d); print "@$e\n";
$ perl sample0911.pl 1 4 9 16 25 36 49 64 81 100 1 8 27 64 125 216 343 512 729 1000 2 4 6 8 10 1 3 5 7 9 55 55 def ghi abc abc ghi