続々・Zend Engine Hack (クロージャもあるよ) (拡張モジュールもあるよ)

拡張モジュール版もできました。普通のPHP extensionとしても使えますし、Zend extensionとしてAPCやeAccelerator等より先にロードすればキャッシュも効きます。ただし、ZEND_APIで修飾されていない (dllexportされていない) 関数を多用しているため、Windows向けにはビルドできません。
モジュール名のQIQは「PHPの次」を意味します。読み方はたぶん「くいっく」。「きゅーあいきゅー」でもおk。小文字でPHPと並べると字面が対照的なのが気に入っています。
dvd btb
qiq php

Windows版はCLICGIの両方をビルドし、モジュールはmbstring, PCRE, SQLite, SimpleXML等を静的に組み込み、GDとeAcceleratorのDLLも同梱しています。
この拡張モジュール・パッチでは下記の機能を提供します。

無名関数

まずはおなじみ(?)の無名関数から。構文は function (引数) { 処理 } で、これ全体が無名関数を表す式として扱われます。式なので、変数に代入するときは $func = function () { return 1; }; と、セミコロンで終端する必要があります。array_map(function($n){return $n*$n;}, $numbers); のように、コールバック関数としても使えます。参照を返す無名関数は書けません。

<?php
$list = range(0, 5);
usort($list, function($a, $b){ return $b - $a; });
print_r($list);

結果:

Array
(
    [0] => 5
    [1] => 4
    [2] => 3
    [3] => 2
    [4] => 1
    [5] => 0
)


create_function()は、ただ直感的に書けないだけでなく、ループで使うと呼び出される度に新しい関数を生成するので、知らないうちにメモリを大量に使用することがあります。例えば、以下のようなコードを実行した場合は、3つの無名関数が生成され、リクエスト終了まで破棄されません。

<?php
$lists = array(range(0, 5), range(5, 10), range(10, 15));
foreach ($lists as &$list) {
    usort($list, create_function('$a, $b', 'return $b - $a;'));
}
print_r($lists);


create_functionでは、関数はまず“__lambda_func”という一時的な名前の関数として評価され、改めて“\0”から始まる、通常のスクリプトではパースエラーとなる名前で再登録されます。
lambda: create_function()
lambda: create_function() posted by (C)rsk


“__lambda_func”は再登録後に破棄されますが、create_function()を呼び出した時に関数__lambda_func()が定義されているとE_ERRORレベルのエラーが発生します。

<?php
function __lambda_func() {}
create_function('', '');
Fatal error: Cannot redeclare __lambda_func() (previously declared in ...


一方、無名関数は1度しかコンパイルされないので、何回ループ中で使っても大丈夫です。もちろん__lambda_func()が定義されていても大丈夫です。

<?php
$lists = array(range(0, 5), range(5, 10), range(10, 15));
foreach ($lists as &$list) {
    usort($list, function($a, $b){ return $b - $a;});
}
print_r($lists);


無名関数は通常の関数と同じく、コンパイル時の一意なキーと、小文字に正規化された呼び出される際のキーで登録されます。シンボルテーブルのエントリは2つありますが、関数の実体であるop_arrayは一つで、参照カウント方式で管理されています。コンパイル時の名前で登録されたエントリのスタティック変数用シンボルテーブルstatic_variablesは常にNULLですが、関数がスタティック変数を持つ場合は、呼び出される名前で登録されているエントリには固有のスタティック変数用シンボルテーブルが割り当てられます。関数呼び出しの際には小文字に正規化されたキーで関数をシンボルテーブルから検索するので、コンパイル時の名前で登録されたエントリが実際に使われることはありません。

lambda: 無名関数・インライン無名関数
lambda: 無名関数・インライン無名関数 posted by (C)rsk


また、無名関数は宣言の直後に()と、必要があれば引数を指定してインライン呼び出しもできるので、PHPの大雑把なスコープに代わるブロックスコープとしても利用できます。インライン呼び出しの際、左辺には無名関数ではなく、無名関数の戻り値が代入されます。

<?php
$var = 'foo';
$ret = function(){ $var = 'bar'; var_dump($var); return $var; }();
var_dump($var, $ret);

結果:

string(3) "bar"
string(3) "foo"
string(3) "bar"

クロージャ

無名関数宣言の前に“static”を付けると、クロージャになります。クロージャは初期化されていないか、値がnullのスタティック変数の値を、宣言されたスコープの同名の変数から取り込みます。

<?php
function get_counter($initial = 0)
{
    $count = $initial;
    return static function(){
        static $count;
        return ++$count;
    };
}

$c1 = get_counter();
$c2 = get_counter(1);
$c3 = get_counter(2);

var_dump($c1(), $c2(), $c3());
echo "-\n";
var_dump($c1(), $c2(), $c3());
echo "-\n";
var_dump($c1(), $c2(), $c3());

結果:

int(1)
int(2)
int(3)
-
int(2)
int(3)
int(4)
-
int(3)
int(4)
int(5)


クロージャは必要とされる度に作成され、オリジナルの無名関数とop_arrayを共有し、独自のスタティック変数用シンボルテーブルを持ちます。create_function()よりコストは低いのですが、反復回数の多いループ中で使うのはおすすめしません。

lambda: クロージャ
lambda: クロージャ posted by (C)rsk


無名関数と同じく、インライン呼び出しもできます。クロージャをインライン呼び出しした場合は、前に登録されていたクロージャを破棄し、同じキーで登録します。これによってメモリの浪費が抑えられるので、ループでも安心して使えます。

<?php
for ($i =0; $i < 5; $i++) {
    var_dump(static function($n){
        static $i;
        return pow($n, $i);
    }($i));
}

結果:

int(1)   // 0の0乗
int(1)   // 1の1乗
int(4)   // 2の2乗
int(27)  // 3の3乗
int(256) // 4の4乗

lambda: インラインクロージャ
lambda: インラインクロージャ posted by (C)rsk


クロージャのスタティック変数はオリジナルから複製されるため、全てのスタティック変数がnull以外で初期化されている場合も、生成されたクロージャは別の関数としてふるまいます。逆に、スタティック変数をもたないクロージャを定義した場合は、E_STRICTレベルのエラーが発生し、複製は行われずオリジナルの関数を返します。また、以下のようにして無理矢理オリジナルのスタティック変数を更新すると、それ以後は期待通りのクロージャが生成されなくなってしまいます (普通はこんなことしないだろうけど、念のため解説)。

<?php
function get_counter($initial = 0)
{
    $count = $initial;
    return static function(){
        static $count;
        return ++$count;
    };
}

$c1 = get_counter();
$c2 = get_counter(1);
$c3 = get_counter(2);

$c0 = substr($c1, 0, strrpos($c1, '.'));
$c0(); // $count が null++ = 1 で固定される

$c4 = get_counter(3); // "3" が$countに代入されない
$c5 = get_counter(4); // "4" が$countに代入されない


キーワードに既存の“static”を使ったのは字句解析器はそのままに、構文解析器の変更だけで対応したかったからです。クラスメソッドの“static”修飾子と紛らわしいとも考えましたが、静的変数の解決に定義されたスコープを使う無名関数だからよしとします。何が「よし」なのか書いている本人も疑問ですが、とにかくよしとしてください。

再帰のために

以前のパッチでは、無名関数の再帰呼び出しために__FUNCTION__の挙動にアドホックな変更をしていたのですが、それを止め、現在の関数がシンボルテーブルに登録されているキーを取得するための関数get_current_function_key()と、キーをローカル変数に代入する関数set_current_function_key()を実装しました。これらの関数では無名関数やクロージャの正しいキーを取得できます。関数外で呼び出された場合はE_WARNINGレベルのエラーが発生し、falseを返します。

<?php
// 1から10までカウントアップ
function($from, $to){
    static $self = false;
    if (!$self) $self = get_current_function_key();
    var_dump($from++);
    if ($from > $to) return;
    $self($from, $to);
}(1, 10);

// 1から10までカウントアップ
// set_current_function_key()はキーを第1引数で指定されたローカル変数に代入し、その変数名を返す
// 引数を省略した場合は“${''}”にキーを代入し、空文字列を返す
function($from, $to){
    var_dump($from++);
    if ($from > $to) return;
    ${set_current_function_key()}($from, $to);
}(1, 10);

new/cloneからはじまるメソッドチェーン

“new ClassName”や“clone $object”を“$()”で囲むと、囲んだ式を変数のように扱うことができます。これによって、$(new SomeClass($param))->method(); とメソッドチェーンを書けるようになります。コンストラクタに引数を渡さないときは $obj = new SomeClass; と同じく、括弧を省略して $(new SomeClass)->method(); と書くこともできます。もちろんメソッドだけでなく、プロパティにも同様にアクセスできます。

角括弧で配列を宣言/リスト代入

array()シンタックスシュガーとして [] が使えるようになります。キーと値の区切りは => です。 list() の代わりに $[] でリスト代入もできます。

<?php
$a = [1, 2, 3];
$b = ['foo' => 'hoge', 'bar' => 'fuga', 'baz' => 'piyo'];
print_r($a);
print_r($b);
$c = 'Hello';
$d = 'World';
$[$c, $d] = [$d, $c];
var_dump($c, $d);

結果:

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
)
Array
(
    [foo] => hoge
    [bar] => fuga
    [baz] => piyo
)
string(5) "World"
string(5) "Hello"

おまけ

無名関数・クロージャには以下のような別の構文もあります。次のリリースでは廃止します。

<?php
$lambda = $(($a, $b){ return $a * $b; }); // 無名関数
$closure = static $((){ static $i; return ++$i; }); // クロージャ