hnwの日記

PHP 5.3の無名関数を試してみた

何番煎じかわからないですが、PHP 5.3からは無名関数が実装されたということで、試しに使ってみました。


見なくても何となく使えるようなものだと思いますが、ドキュメントが「PHP: 無名関数 - Manual」にありますので、ざっと目を通した方がいいと思います。


僕は「どうせ無名関数って中身はcreate_functionなんでしょ?」と思っていたんですが、Closureクラスを使って実装されているなんてことも書いてあります。PHPにしてはマトモっぽくて意外に感じました。

コールバック関数としての無名関数

PHPには引数としてコールバック関数を要求する関数がありますが、PHP5.3.0からは無名関数を引数にしても動くようになりました。


無名関数は、普段の関数と同じノリで「function」から書き始めればOKです。関数宣言との違いは関数名を書かない点だけで、引数やタイプヒンティングについては関数宣言と同様に書けるみたいです。

<?php
$a = array(1,2,3,4,5);

// mapで全要素を10倍
print_r(array_map(function ($v) { return $v*10; }, $a));

// filterで奇数の要素だけ取り出す
print_r(array_filter($a, function ($v) { return $v%2; }));

// reduceで総和を求める
var_dump(array_reduce($a, function ($v,$w) { return $v+$w; }));

// 2進表示した文字列で正順ソート
usort($a, function ($a,$b) { return strcmp(decbin($a), decbin($b)); });

print_r($a);


array_map,array_filter,array_reduce,usortの各関数に無名関数を適用してみました。これを実行すると次の結果が得られます。

Array
(
    [0] => 10
    [1] => 20
    [2] => 30
    [3] => 40
    [4] => 50
)
Array
(
    [0] => 1
    [2] => 3
    [4] => 5
)
int(15)
Array
(
    [0] => 1
    [1] => 2
    [2] => 4
    [3] => 5
    [4] => 3
)


割と直感的に記述できている印象です。array_mapとarray_filterとで引数の順序が逆なのに気づいて少々のけぞりましたが、まあPHPですしね。

クロージャとしての無名関数

さて、次にクロージャを作ってみます。クロージャというのは、その関数が宣言されたスコープで有効な変数が関数と結びついて、変数のスコープ外であってもその関数からは使える機能、とでも言えば良いでしょうか。このようにクロージャから参照される変数のことを「レキシカル変数」とも言います。


正確な説明は「クロージャ - Wikipedia」あたりをご覧ください。


PHPの無名関数でレキシカル変数を扱うには、「function () use($variable)」という文法で利用する変数$variableを宣言する必要があります。少々カッコ悪い文法に見えますが、まずは実験してみましょう。

<?php

function return_closures() {
  $foo = 'foo';
  $bar = function () use ($foo) { $foo.=' bar'; return $foo; };
  $baz = function () use ($foo) { $foo.=' baz'; return $foo; };
  return array($bar, $baz);
}

list($f, $g) = return_closures();

var_dump($f());
var_dump($g());
var_dump($f());


これを実行すると次の結果になります。

string(7) "foo bar"
string(7) "foo baz"
string(7) "foo bar"


$fや$gの呼び出し時点では$fooのスコープ外ですが、$fooの値である'foo'は利用できています。


しかし、$fooの値は呼び出しごとに書き変わらないようです。僕は$fooが2つの関数で共有されると想像していたので少々意外な結果に感じました。少し調べたところ、次のようにすれば2つの関数で同じ変数値を共有できることがわかりました。

<?php

function return_closures_using_reference() {
  $foo = 'foo';
  $bar = function () use (&$foo) { $foo.=' bar'; return $foo; };
  $baz = function () use (&$foo) { $foo.=' baz'; return $foo; };
  return array($bar, $baz);
}

list($f, $g) = return_closures_using_reference();

var_dump($f());
var_dump($g());
var_dump($f());
string(7) "foo bar"
string(11) "foo bar baz"
string(15) "foo bar baz bar"


関数の引数宣言と一緒で、レキシカル変数はデフォルトでコピー渡しされてしまうみたいですね。&をつければ参照渡しになるので、同じ変数値を共有できるというわけです。想像ですが、この両者を区別するために「use()」という文法が必要だったのかもしれません。


ちなみに、レキシカル変数としてオブジェクトを指定すると参照渡しになります。これも通常の関数呼び出しと同じですね。