関数オブジェクトと関数型言語的手法の違い

PHPで関数オブジェクトを作ってみました。Webフレームワークでのキャッシュコントロールを正確に行うのが目的なのですが、作ってみると関数型言語との手法に違いがあることがわかりました。

関数オブジェクトAbstract*1

関数型の手法を几帳面にエミュレートすることもできるのですが、引数の仕様部分を独自仕様にしています。
abstractの仕様は簡単な関数を引数を受け取って何かを返すと定義してみました。

  • 関数オブジェクトは、引数をオブジェクトに保持する
  • operatorメソッドでその引数を処理して、定義通りの結果を返す

これにより、引数割り当てを、コンストラクタもしくはapplyメソッドで行い、任意のタイミングでoperatorを呼ぶことで遅延評価可能な関数オブジェクトの抽象型ができあがります。
関数型言語の仕様と大きく異なるのは、引数の順番ではなく、ハッシュのインデックスで処理するところです。この部分はカリー化を行う時に大きな違いになって現れますが、その部分を厳密に関数化するよりも現実用途との間で妥協して変化してみました。

<?php
interface Flower_Functor_Interface
{
    public function apply();
    public function operator();
}
abstract class Flower_Functor_Abstract implements Flower_Functor_Interface
{
    protected $_params = array();
    protected $_bindParams = array();
    private   $_fields;
    protected $_fieldsCount;
    protected $_valid = false;
    /**
     * flag for overloading params
     *
     */
    const OVERLOAD = 0x01;
    public function __construct(array $params = array())
    {
        $this->_valid = false;
        $this->_fieldsCount = count($this->_fields);
        $this->_bindParams = $params;
        $this->apply($params);
    }
    public function apply(array $params, $flag = 0)
    {
        if (count($params) > 0) {
            $qParams = array_intersect_key($params, $this->_fields);
            if ($flag & self::OVERLOAD) {
                $this->_params = array_merge($this->_params, $qParams);
            } else {
                $this->_params += $qParams;
            }
        }
        if (count($this->_params) === $this->_fieldsCount) {
            $this->_valid = true;
        }
        return $this;
    }
    public function isValid()
    {
        return $this->_valid ? true : false;
    }
    public function getParams()
    {
        return $this->_bindParams + $this->_params;
    }
    abstract function operator ($params = null, $flag = 0);
}

operatorの実装のシンプルな例。引数をリストして返すだけです。実用性はありません。
ただ、大事なことは、operator内で行っていいのは、引数に基づいた処理結果を「返す」ことだけです。
それ以外の他のオブジェクトに対する操作や入出力は基本的に行わないという「約束事」を守る前提となります。そのため、operatorを直接使用するのは避けた方がいいかもしれません。

<?php
    public function operator()
    {
        return var_export($this->getParams(), true);
    }

fieldsをハッシュ配列で設定しておけば、引数を与えるタイミングを任意に設定し、処理結果を最後に得るということが可能になります。これがキャッシュ管理には好都合なわけです。

基底クラスを作る

抽象型をやや実務的に拡張して基礎になるクラスを書いてみます。実際に関数の処理部分にあたるoperatorを外に切り出して、関数オブジェクトの作成と、カリー化*2を同時に実装しました。
operatorの返りが関数か値かという違いがあるあたり、関数脳な人は発狂するかもしれませんが、ここでは、operatorはapplyを含んで、引数が揃い次第則実行しながら進められるようにしています。

<?php
class Flower_Functor extends Flower_Functor_Abstract
{
    protected $_operator;
    public function __construct(Flower_Functor_Operator_Interface $operator
                              , array $fields = array()
                              , array $params = array())
    {
        $this->_operator = $operator;
        $this->_fields = $fields;
        parent::__construct($params);
    }
    public function operator(array $params = null, $flag = 0)
    {
        if ($params) {
            $this->apply($params, $flag);
        }
        if ($this->isValid()) {
            return $this->_operator->operate($this->getParams());
        } else {
            return $this;
        }
    }
    public function curry($params)
    {
        $fields = array_diff_key($this->_fields, $params);
        //$instance = clone $this;
        $instance = new self($this->_operator, $fields, $params + $this->_params);
        return $instance;
    }
    public static function factory($operator, array $params = array())
    {
        $fields = $operator->getFields();
        return new self($operator, $fields, $params);
    }
}

Operatorを書いてみる

関数の処理部分を書いてみます。
Operatorでは他のメソッドをなるべく実装しないようにしたいところです。Operatorのoperateはなるべくシンプルにし、定義した引数以外は受け取らない、使わないということを徹底する必要があります。そうでないと参照透過性が失われて、関数オブジェクトにしている意味がなくなります。

<?php
class Flower_Functor_Operator_Sum extends Flower_Functor_Operator_Abstract
{
    protected $_fields = array('a' => 'int', 'b' => 'int');
    public function operate($params)
    {
        return $params['a'] + $params['b'];
    }
}

$_fieldsは引数としてa、bともにintでくださいねと。returnの型指定はしていません。operateは引数を足し算して返します。単純ですね。

実装する

<?php
//足し算を行う関数オブジェクトを作成します。
$operator = new Flower_Functor_Operator_Sum();
$functor = Flower_Functor::factory($operator, array());

//引数を一つだけ与えてみます。
$functor->apply(array('a' => 5));

//カリー化した$newFuncを作ります。この$newFuncは引数bしか取らず、a=>7は固定でsumを呼びます。
$newFunc = $functor->curry(array('a' => 7));
$functor->apply(array('b' => 6));
$newFunc->apply(array('b' => 6));
//Zend_Debug::dump($newFunc);

//出力します。
echo "case 1: " . $functor->operator() . "<br />\n"; //結果11
echo "case 2: " . $newFunc->operator() . "<br />\n"; //結果13

カリー化するかどうかで、新しい関数オブジェクトを育成しているかどうか、引数のオーバーロードが可能かどうかなどの違いが出ます。

なんちゃって関数オブジェクトの関数型言語手法との決定的な違い。

PHP5.3からクロージャーが使えますので、クロージャーを使えばこの辺の書き方はスマートにできて、パフォーマンスも上がります。関数オブジェクトで関数型と同じ動作を書こうとすると、オブジェクト化するコストが高くて実用性を損ないます。
そこで、仕様を崩してオブジェクトに使い方を合わせ、多様化する方向で考えてみました。一見すると普通のオブジェクト指向をめんどくさくしただけのように見えるかもしれませんが、引数・処理・結果だけを抽出し、一つのインターフェースに集約することで実用性を向上させようという試みです。

実装に生かす

まずは、任意の時点で引数を与えられることを利用して、ActionControllerの呼び出しを非同期的に処理できるようにすること、評価タイミングをコントロールすることで、キャッシュコントロールをやりやすくするという方向で利用してみたいと思います。

*1:関数オブジェクトをFunctorと名付けていますが、数学の圏論でのFunctorとは別物です

*2:ここでのカリー化とは、本来の意味とは少し変えて、引数をバインドして残りの引数をとる関数オブジェクトを育成するという意味で使用しています