読者です 読者をやめる 読者になる 読者になる

PHPのコールバックを使いやすくする

PHPのコールバックは少し使い辛いので、それを和らげるようなスニペットを実験的に書いた。
コードはこんな感じ。

<?php

class Curry
{
    protected $callback, $bind;
    protected function __construct($callback, Array $bind)
    {
        if(!is_callable($callback)) throw new InvalidArgumentException;
        list($this->callback, $this->bind) = func_get_args();
    }
    static function make($callback, $bind)
    {
        return array(new self($callback, $bind), 'invoke');
    }
    function invoke()
    {
        $args = func_get_args();
        return call_user_func_array($this->callback, array_merge($this->bind, $args));
    }   
}

class Quotation
{
    protected $obj;
    function __construct($obj)
    {
        $this->obj = $obj;
    }
    function __get($name)
    {
        return array($this->obj, $name);
    }
    function __call($name, $args)
    {
        return Curry::make($this->{$name}, $args);
    }
}

function quote($obj)
{
    return new Quotation($obj);
}

function callee()
{
    $trace = debug_backtrace();
    if (!isset($trace[1])) throw new BadFunctionCallException;
    $frame = $trace[1];
    
    $callback = isset($frame['object']) ? array($frame['object'], $frame['function']) :
               (isset($frame['class']) ? array($frame['class'], $frame['function']) : 
               $frame['function']);
    $args = func_get_args();
    
    return $args ? Curry::make($callback, $args) : $callback;
}


function method($name)
{
    $trace = debug_backtrace();
    if (!isset($trace[1]['class'])) throw new BadFunctionCallException;
    
    $callback = array(isset($trace[1]['object']) ? $trace[1]['object'] : $trace[1]['class'], $name);
    $args = func_get_args();
    array_shift($args);
    
    return $args ? Curry::make($callback, $args) : $callback;
}


function call($callback)
{
    $args = func_get_args();
    array_shift($args);
    return call_user_func_array($callback, $args);
}

function apply($callback, $args)
{
    return call_user_func_array($callback, $args);
}

まず、callとapplyはcall_user_func_arrayというタイプ数を著しく消費する関数のラッパーで、見たまんまなので説明は省き、以下ではquote, method, calleeという三つの関数について説明する。

quote関数

まず、quote関数はあるオブジェクトのメソッドのコールバックを取り出すために使う。

<?php
class Hoge
{
    function piyo($str)
    {
        echo $str;
    }
}
$callback = quote(new Hoge)->piyo; 

quote関数はQuotationクラスのコンストラクタのラッパなんだけど、こいつはカリー化も出来る。

例えば、以下のように使える。

<?php
$curry = quote(new Hoge)->piyo('gyaaaaa');
call($curry); //=> 'gyaaaaa'と表示される

method関数

次にmethod関数だが、これはクラス内でメソッドのコールバックを取り出すのに使う。

<?php
class Fuga 
{
    function hoge()
    {
        return method('piyo'); // piyoメソッドのコールバックが取り出される
    }
    function piyo($str)
    {
        echo $str;
    }
}

method関数の内部ではバックトレースを利用してオブジェクトを指定しなくとも良いようになっている。staticメソッド内で呼び出した場合は、クラスメソッドのコールバックが取り出される。

このmethod関数もquoteと同様にカリー化できる。

<?php
class Fuga 
{
    function hoge()
    {
        return method('piyo', 'gyaaaa'); // piyoメソッドに続く引数をバインドしたコールバックが取り出される
    }
    function piyo($str)
    {
        echo $str;
    }
}
$fuga = new Fuga;
call($fuga->hoge()); // 'gyaaaa'と表示される

callee関数

最後にcallee関数は自分自身のコールバックを取り出すのに使われる。method関数と同じくこれも内部でバックトレースを使っていて、例えばfoo関数の中で呼び出すとfoo関数のコールバックが得られ、barメソッドの中で呼び出すと同様にbarメソッドのコールバックが得られる。
当然、このcallee関数もカリー化ができ、例えばPHPで無限リストを実装したい場合は以下のように書ける。

<?php
function ilist($i = false)
{
    if ($i === false) return callee(1);
    return array($i, callee($i * 2));
}

// 無限リストから10回値を取り出す
$callback = ilist();
for ($i = 10; $i-- > 0;) {
    list($result, $callback) = call($callback);
    echo $result . PHP_EOL;
}

/* 実行結果は以下
1
2
4
8
16
32
64
128
256
512 */

最後に

callback.php
gistにもコード置いてるんで暇な方はいじってみてください。