プログラミング

PHPでクロージャもどき

2008年3月25日

クロージャとは何かと言う話。

これを知るため、Googleで『クローじゃ』もとい『クロージャ』で検索すると、最上位に出るのがこれ。

クロージャ - Wikipedia

これによると、

クロージャ (Closure) は、プログラミング言語において引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決する関数のことである。

とのこと。ちなみに、『クローじゃ』で検索すると、次のものが出る。

PHPでクローじゃもどき - odz buffer

PHPでクローじゃクロージャが使えないというのは、どこかよその場所でも見た(だから、クロージャについて調べたんだけれど)。

Wikipediaで紹介されているクロージャの使用例(JavaScript)は次のもの。

function newCounter() {
    var i = 0;
    return function() { // 無名関数
        i = i + 1;
        return i;
    }
}

c1 = newCounter();
alert(c1()); // 1
alert(c1()); // 2
alert(c1()); // 3
alert(c1()); // 4
alert(c1()); // 5

良く分からないが、これがちゃんと実行できれば、クロージャが使えるということなのだろう。この解釈は、Wikipediaに書いてあるクロージャの定義とも合う。

『PHPでクローじゃもどき 』のリンク先で紹介されていた方法(文字列を『"』で囲って、create_functionの実行前にパースする方法)以外の方法を考えてみた。まず、上のJavaScriptのコードを、そのままPHPに移植してみる。

<?php

function newCounter() {
    $i = 0;
    return create_function('', '
        $i=$i+1;
        return $i;
    ');
}

$c1 = newCounter();
echo $c1().'<br>'; // 1
echo $c1().'<br>'; // 1
echo $c1().'<br>'; // 1
echo $c1().'<br>'; // 1
echo $c1().'<br>'; // 1

?>

これは、ちゃんと実行されない。つまり、無名関数$c1は常に1を返している。newCounterで定義されている、『$i』が、その無名関数に受け渡されていないからだ。

ならば、クロージャの定義に従って、無名関数に$iが受け渡されるようにすれば良いのであろう。で、それ用のクラスを作ってみた。

<?php

function newCounter() {
    $i = 0;
    return eval(closure::create_function('', '
        $i=$i+1;
        return $i;
    '));
}

$c1 = newCounter();
echo $c1().'<br>'; // 1
echo $c1().'<br>'; // 2
echo $c1().'<br>'; // 3
echo $c1().'<br>'; // 4
echo $c1().'<br>'; // 5

class closure{
    static public $vars=array();
    const SEARCH_VAR='/\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/';
    static public function create_function($args,$code){
        static $num=0;
        $a=array();
        preg_match_all(self::SEARCH_VAR,$code,$m,PREG_SET_ORDER);
        foreach($m as $match) $a[$match[0]]=true;
        preg_match_all(self::SEARCH_VAR,$args,$m,PREG_SET_ORDER);
        foreach($m as $match) unset($a[$match[0]]);
        $code1=$code2='';
        foreach($a as $arg=>$value){
            $code1.="closure::\$vars[{$num}]=&{$arg};\n";
            $code2.="{$arg}=&closure::\$vars[{$num}];\n";
            $num++;
        }
        $args=addslashes($args);
        $code=str_replace(array('\\',"'"),array('\\\\',"\\'"),$code2.$code);
        return "{$code1}return create_function('{$args}','{$code}');";
    }
}

?>
(080327改定)

これなら、ちゃんと動く。無名関数$c1は、呼び出されるごとに1つずつ大きな値を返す。Wikipediaに書かれている定義については、ちゃんと実装されていることになる。

『PHPでクローじゃもどき』からのリンク先のページで紹介されていた、ちゃんと動かないコード

function adder($x) { return create_function('$y', 'return $x + $y;'); }
$f = adder(1);
echo $f(2);

これも、次のようにすると動く。

function adder($x) { return eval(closure::create_function('$y', 'return $x + $y;')); }
$f = adder(1);
echo $f(2);

こんなので、PHPでクロージャが使えていると言えるのかな?もしそうなら、PHPにおいてクロージャ用のcreate_functionを実装すればよい(create_closureとか)のだと思うのだけれど、それは簡単ではないということなのか、需要があまり無いので手をつける人がいないということなのか?

(080327追記)

上記newCounter()の例では、closure::create_function()メソッドは次の文字列を返す。

closure::$vars[0]=&$i;
return create_function('','$i=&closure::$vars[0];

        $i=$i+1;
        return $i;
    ');

コメント

hsur (2008年3月26日 02:53:51)


Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51

Warning: comments::cb1_tag_body(): Argument #1 ($m) must be passed by reference, value given in /home/u109394186/domains/rad51.net/public_html/jeans/jeans/libs/comments.php on line 51
僕もそんなにちゃんと理解しているわけではないという前提ですが。。。。

おそらく簡単なものであれば、上記のcreate_closureでも十分実用的だと思いますが、同一スコープにおいて、レキシカル変数を参照する関数が複数存在したり、スコープが入れ子になってきたりすると言語的に実装されていないと苦しくなると思います。

たとえばこんな例とか。。。。。

count = 0;
function newCounters() {
    var i = 0;
    var f1 = function() { // 無名関数
count++;
        i = i + 1;
        return i;
    }
    var f2 = function() { // 無名関数
count++;
        i = i + 2;
        return i;
    }
    
    return [ f1, f2 ];
}

cs1 = newCounters();
cs2 = newCounters();

alert(cs1[0]()); //1
alert(cs1[0]()); //2
alert(cs1[1]()); //4
alert(cs1[1]()); //6

alert(cs2[0]()); //1
alert(cs2[0]()); //2
alert(cs2[1]()); //4
alert(cs2[1]()); //6

alert(cs1[0]()); //7
alert(cs1[0]()); //8

alert(count); //10

Katsumi (2008年3月26日 11:44:39)

JavaScript の var がスタティックだと勘違いしていました。ローカルですね。PHPだと、関数内で何も指定しない場合に相当するので、それに従って記事中のコードを書き換えました。結論としては、これでも動いています。

hsurさんの上記のJavaScriptコードをPHPに移植すると下記の様になり、これも予想される動作をしました。

たとえローカル変数でも、&$localvar として参照をどこかに残すと(今の例では、closureクラスのスタティック変数ですが)、そのローカル変数はちゃんと残るようです(しかも、2回目以降の関数呼び出しでは、別のローカル変数が作成されます)。あるいは、そんなことをしなくても変数は残っているんだけれど、あとでGCがそこに来たときに、どこかで参照されている変数は削除しないのかもしれません。

<?php

$count = 0;
function newCounters() {
global $count;
$i = 0;
$f1 = eval(closure::create_function('','
$count++;
$i = $i + 1;
return $i;
'));
$f2 = eval(closure::create_function('','
$count++;
$i = $i + 2;
return $i;
'));

return array( $f1, $f2 );
}

$cs1 = newCounters();
$cs2 = newCounters();

alert($cs1[0]()); //1
alert($cs1[0]()); //2
alert($cs1[1]()); //4
alert($cs1[1]()); //6

alert($cs2[0]()); //1
alert($cs2[0]()); //2
alert($cs2[1]()); //4
alert($cs2[1]()); //6

alert($cs1[0]()); //7
alert($cs1[0]()); //8

alert($count); //10

function alert($text){
echo htmlspecialchars($text)."<br>\n";
}

Andy (2008年4月1日 22:38:04)

僕はほとんど門外漢ですが,クロージャ,クロージャと騒ぐのは言語屋だけで,実際にはそんなに使われていないのではないか,という気がします。

オブジェクト指向の機能と比べたら明らかになくても困らなさそうだし。

Kat (2008年4月2日 14:28:18)

おっしゃる通りです。色々調べると、オブジェクトの概念でクロージャでできることはほとんどできるようですね。どうしてもクロージャで無いといけないという考え方は、コードをきれいにすること以外に目的が思いつきません。

1)私は、関数内のスタティック変数を良く使います。同じ関数を何回も呼び出すとき、関数の外では不必要なパラメータを保管しておくのに便利なので。ただ、このやり方では、同じ関数を2つ以上の異なる目的別に使い分けるということができません。

2)そういった場合に、ラムダを使うことになるのだと思いますが、PHPの場合だとラムダ内でもスタティック変数が容易に利用できるので、まだクロージャの出番ではないです。

3)2よりもより複雑で、かつ、オブジェクトを作成するほどでもないケースで、クロージャが活躍するのかなと、勝手に想像しています。そのような用途に出くわす頻度は、あまり高くないように思いますね。

4)PHPのcreate_functionのクロージャ様の使い方について、色々なサイトで記述がありますが、ほとんどはcreate_functionの第二引数にダイナミックにコードを渡すというものです。これはクロージャの利用方法の一つのように見受けられますが、本格的なクロージャだと、他にも用途は有るのでは無いかと思ってます。

オブジェクト以外にもこんなのが有るよと頭の片隅においておけば、何かをプログラミングしている時にふっと思い出すかもしれない…と思ってます。

ハルヒト (2009年9月15日 06:05:14)

すごいですね。PHPにおける実装。僕も考えたんですが、やはりstaticな領域を保持することになりますよね。

クロージャや高階関数、カリー化はやはりかなり使い勝手のいい概念であり、いい機能だと思いますよ。

オブジェクト指向で全て書くのは、冗長になりすぎますからね。

コメント送信