CakePHP 用のモナドプラグイン Monaca

CakePHPモナドのライブラリ移植した。モナドがよくわかってないけど、Maybe ぐらいは使えないと今後やっていけないんじゃないかと思うぐらいに便利そうなので、勉強がてらそのへんに転がっていたライブラリを CakePHP2 に移植してみた。
ソースコードこちら

使い方はとりあえず参考にしたライブラリの方の記事を訳す。

使い方

値をモナドでラッピングするには、コンストラクタか unit 関数を用いる。ラッピングしたモナドに対しては bind 関数を用いて関数呼び出しを行う。

App::uses('Identity', 'Monaca.Lib');
$monad = new Identity(1);
$monad->bind(function($value) { var_dump($value); });
// int(1) が表示される

bind 関数を用いた関数呼び出しは、新しくモナドインスタンスでラッピングされた値を返す。

App::uses('Identity', 'Monaca.Lib');
$monad = new Identity(1);
$monad->bind(function($value) {
        return 2 * $value;
    })->bind(function($value) {
        var_dump($value);
    });
// Prints int(2)

PHP は純関数型言語ではないので、生の値を取り出す実装もしてある。

App::uses('Identity', 'Monaca.Lib');
$monad = new Identity(1);
var_dump($monad->extract());
// Prints int(1)

Scala にある Option みたいな取り方も実装した。

  • get($key)
  • getOrElse($key, $default)
  • getOrCall($key, callable $function)
  • getOrThrow($key, Exception $exception)

第一引数のキーは、値が配列の場合にオフセットかキーを指定する。スカラ値の場合は null を渡せばいい。

Maybe モナド

有用なモナドのうちのひとつは Maybe モナドだ。Null である可能性がある値をラッピングすることでヌルポを防ぐ。

App::uses('Maybe', 'Monaca.Lib');
$monad = new Maybe(1);
$monad->bind(function($value) { var_dump($value); });
// int(1) が表示される

$monad = new Maybe(null);
$monad->bind(function($value) { var_dump($value); });
// 処理されない

List モナド

リストに何か処理をしたい場合はこれを使うといい。

App::uses('ListMonad', 'Monaca.Lib');
$monad = new ListMonad(array(1, 2, 3, 4));
$doubled = $monad->bind(function($value) { return 2 * $value; });
var_dump($doubled->extract());
// array(2, 4, 6, 8) が表示される

ただし、このクラスを用いる場合は配列以外を渡すと例外がおこるので気を付けること。
また、連想配列の場合は HashMonad を使う。

Defered

バッチかなんかでは Deferred も有用だ。

App::uses('Deferred', 'Monaca.Lib');
$promise = new Deferred();
$success = function($result) { Log('success!'); };
$failure = function($result) { LogError('error!'); };
$promise->when($success, $failure);
$some_long_process = function($promise) {
    // 処理が成功
    $result = 'whatever the process result';
    if (true) {
        $promise->succeed($result);
    } else {
        $promise->fail($result);
    }
}

コンポジション

これらのモナドの中身はさらにモナドでラッピングでき、非常に有用である。

$monad = new ListMonad(array(1, 2, 3, null, 4));
$newMonad = $monad->bind(function($value) { return new Maybe($value); });
$doubled = $newMonad->bind(function($value) { return 2 * $value; });
var_dump($doubled->extract());
// array(2, 4, 6, null, 8);

これが多次元配列だった場合も

$monad = new ListMonad(array(array(1,2), array(3,4), array(5,6));
$newMonad = $monad->bind(function($value) { return new ListMonad($value); });
$doubled = $newMonad->bind(function($value) { return 2 * $value; });
var_dump($doubled->extract());
// array(array(2, 4), array(6, 8), array(10, 12))

また有用なunit関数のコールバック定数も用意してある。

$newMonad = $monad->bind(Maybe::UNIT);
// $newMonad = $monad->bind(function($value) { return new Maybe($value); }); と同じ意味

実践

例えば、下記のような多次元配列を処理する場合

$posts = array(
    array("title" => "foo", "author" => array("name" => "Bob", "email" => "bob@example.com")),
    array("title" => "bar", "author" => array("name" => "Tom", "email" => "tom@example.com")),
    array("title" => "baz"),
    array("title" => "biz", "author" => array("name" => "Mark", "email" => "mark@example.com")),
);

このような値の特定の値が欲しい場合、まず下記のような関数を実装する。

function index($key) {
    return function($array) use ($key) {
        return isset($array[$key]) ? $array[$key] : null;
    };
}

そしてモナドを使う。

$postMonad = new ListMonad($posts);
$names = $postMonad
    ->bind(Maybe::UNIT)
    ->bind(index("author"))
    ->bind(index("name"))
    ->extract();

モナドを使わないでの実装は、こんな感じになるはず。

$names = array();
foreach ($posts as $post) {
    if (isset($post['author'])) {
        if (isset($post['author']['name'])) {
            $names[] = $post['author']['name'];
        }
    }
}

その他CakePHP用に追加した関数

CakePHPモナドっぽく取得したらうまそうなやつを実装した。
・セッションの取得
・コンフィグの取得
・POST/GETパラメタの取得
・View の値の取得方法

ざっくり、MonadController を継承すると下記のような関数が使えるようになるので、AppController にこいつを継承させて使うことをお勧めする。ちなみに、CakePHP よろしくキーを 'foo.bar' のような形で指定しても array( 'foo' => array('bar' => 'baz') ) のようなオブジェクトのキーを指定できる。

HogeController.php

App::uses('MonadController', 'Monaca.Lib');

class HogeController extends MonadController {

    public function index() {
        // セッションをオプショナルに取得できる
        $aSession = $this->getSession('a');
        $bSession = $this->getSessionOrElse('b', 'default value');
        $cSession = $this->getSessionOrCall('c', function() { LogError('セッション C がないです'); return 'default'; });
        $dSession = $this->getSessionOrThrow('d', new Exception('セッションDがないです'));

        // コンフィグをオプショナルに取得
        // こちらも OrElse, OrCall, OrThrow を使える。用例は割愛。
        $aConfig = $this->getConfig('a');

        // POST/GET パラメタをオプショナルに取得
        // こちらも OrElse, OrCall, OrThrow を使える。用例は割愛。
        $aPost = $this->getPost('a');
        $aGet = $this->getQuery('a');

        // POST/GET どちらかを取得、同じキーがある場合はPOSTが優先
        // こちらも OrElse, OrCall, OrThrow を使える。用例は割愛。
        $aInput = $this->getInput('a');
    }
}

同じことが View でもできる。Controller から渡された値がない場合のデフォ値を利用できる。
こっちも getOrCall, getOrThrow は割愛。

<ul>
    <li><?php echo $this->getOrElse('hoge', 'fuga'); ?></li>
</ul>

もしコントローラーが継承できない場合はコンポーネントにしてあるのでそれを使う。ビューはビュークラスを指定することで使える。

App::uses('MonadComponent', 'Monaca.Controller/Component');
App::uses('MonadView', 'Monaca.View');

class FooController extends Controller {
  $components = array('Monaca.Monad'); // MonadComponent をロードする

  public function __constructor($request = null, $response = null) {
    parent::__construct($request, $response);

    // viewClass を指定することで MonadView が使える
    $this->viewClass = 'Monaca.Monad';
  }
}

まとめ

結論としては、自分の認識としてはモナドは依然としてよくわからない。けど、どこかの誰かが言ってたけど、モナド自体はわからなくてもモナドは使えるだけで良いのではないか、という話。確かにモナドは知らなくても、上記のサンプルがあれば使えそう。
適当に使ってるうちに多少理解深まるはず。