PHPを勉強し始めたら、PHPにもクロージャーがサポートされたというので、良く使われているPerl、Ruby、PHPの三スクリプト言語のクロージャー(ラムダ関数または無名関数)を比較してみようと思います。
クロージャーというものがどういうものかというと、Perlで使われているクロージャーが最初に広く使われているので、それから説明いたします。
@array = (1,2,3,4,5);
@ret = grep{ $_ >= 3;} @array; # この{}がクロージャー
print join(",", @ret); # 3,4,5
2行目のgrep文の中括弧({})で囲まれた部分がクロージャーです。クロージャーは、一種のサブルーチン内のサブルーチンとして働きます。ローカルなサブルーチンなので、外側にある変数を参照することができます。たとえば、外部の変数$limitに閾値を入れて実行するようなこともできます。
$limit = 4;
@array = (1,2,3,4,5);
@ret = grep{ $_ >= $limit;} @array;
print join(",", @ret); # 4,5
grepやsortなどのクロージャーは比較的内部だけで完結することが多いクロージャーです。色々な使い方をされるmapを使って比較しましょう。
簡単な例として、mapを使って合計を計算します。map文は各要素についてそれぞれ実行する命令です。返ってきた値で、新しい配列を作成してそれを返します。ここでは、配列を渡してその二乗和を求めてみます。
#perl ex01.pl
$sum = 0;
@array = (1,2,3,4,5);
map{ $sum += $_ * $_ ;}@array;
print "$sum\n"; # =55
実行します。
>ex01.pl
55
期待した値が返ってきていることが確認できました。
Rubyの場合は、map!は、元からある配列を変更してまう命令なので、代わりにeachを使用します。 // ruby ex01.rb
sum = 0
array = [1,2,3,4,5]
array.each{|v| sum += v * v }
print "#{sum}\n" // =55
実行します。
>ex01.rb
55
期待した値が返ってきていることが確認できました。
さて、肝心のPHPのコードを書いてみましょう。
# PHP ex01.php
<?php
$ary = array(1,2,3,4,5);
$sum = 0;
array_map( function ($val){ $sum += $val * $val;}, $ary );
print "$sum\n";
?>
実行すると、以下のようなエラーが表示されます。
PHP Notice: Undefined variable: sum in C:\demo\demo.php on line 4
おや?
PHPはもともと内部関数という概念がないので仕方がないかも知れません。(^_^;)
新しく導入されたキーワードuseを使ってみましょう。
# PHP ex01.php
<?php
$ary = array(1,2,3,4,5);
$sum = 0;
array_map( function ($val) use($sum) { $sum += $val * $val;}, $ary );
print "$sum\n";
?>
実行してみます。
>ex01.php
0
おや、まだ旨くいきませんね。ちょっと途中を表示させてみましょう。
# PHP ex01.php
<?php
$ary = array(1,2,3,4,5);
$sum = 10; # ちょっと別な値を入れてみる
array_map( function ($val) use($sum) {
print "$val $sum\n";
$sum += $val * $val ;
}, $ary );
print "$sum\n";
?>
1 10
2 10
3 10
4 10
5 10
10
うーん、値が変化しません。どうやら、use文で渡しているの変数の値を渡しているようです。
考えてみるとPHPには、ローカルサブルーチンというか、サブルーチンの入れ子って考え方が無かったようです。もともとarray_map()なんかでは、外部に置いた関数を名前で呼び出すような仕様になっていました。クロージャーをサポートしたと言っても、その焼き直しでしかないようです。
仕方がないので、globalキーワードを使用してみましょう。
# PHP ex01.php
<?php
$ary = array(1,2,3,4,5);
$sum = 0;
array_map( function ($val) {
global $sum;
$sum += $val * $val;
}, $ary );
print "$sum\n";
?>
>ex01.php
55
やっとうまくいきました。
しかし、全体を関数にして独立させた場合など、思わぬ副作用が出てしまいました。
# PHP ex01.php
<?php
function test1(){
$ary = array(1,2,3,4,5);
$sum = 0;
array_map( function ($val) {
global $sum;
$sum += $val * $val;
}, $ary );
print "$sum\n";
}
test1();
print "$sum\n";
?>
>ex01.php
0
55
どうにかできないでしょうか?
多少大仰な方法としては、クラスを作ってオブジェクトを渡すという方法が取れるようです。オブジェクト変数は、オブジェクトへの参照なので、useで値渡しになっても実際上は参照として扱われます。
# PHP ex02.php
<?php
class CValue{
public $v ;
};
function test1(){
$ary = array(1,2,3,4,5);
$sum = new CValue;
$sum->v = 0;
array_map( function ($val) use($sum) {
$sum->v += $val * $val;
}, $ary );
print "$sum->v\n";
}
test1();
print "$sum\n";
?>
もう少し簡単な方法もあるはずだと思って、PHPのマニュアルを読んでいると関数へのリファレンス渡しの方法が載っていました。useでも、リファレンス渡しをして見ましょう。
<?php
# PHP ex02.php
$ary = array(1,2,3,4,5);
$sum = 0;
array_map( function ($val) use( &$sum ){ # &が注意
$sum += $val * $val;
}, $ary );
print "$sum\n";
?>
>ex01.php
55
お、うまく行きました。
[結論]
PHPのクロージャーは、PerlやRubyと異なり、外部サブルーチンの個性が色濃く残っています。明示的に内部で使用する変数を渡す必要があります。値を返したい場合は、値渡しであることを明示する必要があります。