PHP 5.3 がリリースされてもう 5 年以上経ちます。
5.3 で無名関数 (Closure) が使えるようになってからは、高階関数を使うなどした関数型っぽいアプローチの可能性が広がりました。
中でも nikic/iter は関数型のコレクション操作を行う上で非常に便利なライブラリです。
ですが、関数を単純にラップする記法では、関数を重ねたときに読みづらく感じる人もいるでしょう。
オブジェクト指向的なメソッドチェインと違って、読み下す順番と、実際の実行順序が逆行するところに原因がある気がします。
以下は 1 から 10 までの数列の全要素を 2 倍してから全て足し合わせる例です。
use function iter\fn\operator;
use function iter\map;
use function iter\range;
use function iter\reduce;
echo reduce(operator('+'), map(operator('*', 2), range(1, 10)), 0), PHP_EOL;
// => 110
言語によってはもっと簡略な記法で関数が定義できたり、より読みやすい記法 (Haskell の中置記法とか) が用意されている場合もありますが、PHP にはそういったものはありません。
(HHVM には Closure の簡略記法がありますね)
というわけでこれをライブラリで解決してみました。
Piper
ソースとなる値を関数に適用して、その返り値を次の関数に適用して... というのをメソッドチェインで記述するためのライブラリです。
UNIX のパイプラインのようなイメージです。
使い方
さっきと同じロジックを記述してみましょう。
全ての関数を pipe()
メソッドでつないでいきます。
use yuyat\Piper;
use function iter\fn\operator;
use function iter\range;
$result = Piper::from(range(1, 10))
->pipe('iter\map', [operator('*', 2)])
->pipe(function ($iter) { return reduce(operator('+'), $iter, 0); })
->get();
echo $result, PHP_EOL;
// => 110
読み下し順通りに実行されるので、さっきの例より内容を理解しやすいのではないでしょうか。
引数が複数あるときは、pipe()
メソッドの第二引数として array
で渡すことができます。
パイプラインを通ってくる値は常に最後の引数として渡されます。
Piper をカスタマイズする
デフォルトではインスタンスメソッドは pipe()
と get()
しかありませんが、これを拡張してみます。
<?php
use yuyat\Piper;
use function iter\fn\operator;
use function iter\range;
use function iter\map;
use function iter\reduce;
class IterPiper extends Piper
{
public function map($fn)
{
return new static(map($fn, $this->get()));
}
public function reduce($fn, $initial = null)
{
return new static(reduce($fn, $this->get(), $initial));
}
}
$result = IterPiper::from(range(1, 10))
->map(operator('*', 2))
->reduce(operator('+'), 0)
->get();
echo $result, PHP_EOL;
// => 110
map()
と reduce()
を Piper
のメソッドとして実装し、iter
をラップする形で利用しています。
ちょっと ginq っぽくなりましたね。