PHPのコレクション処理ライブラリUnderbar.phpの紹介

@emonkak

どんなライブラリ?

何で作ったの?

Arrayを返す実装の例

// 配列を返す実装のクラスをuseする
use Underbar\ArrayImpl as _;

// 要素を2倍にする関数
$double = function($x) {
    return $x * 2;
};

// 要素を2倍にする
$xs = _::map([1, 2, 3, 4, 5], $double);
echo _::join($xs);  // 2,4,6,8,10

Iteratorを利用した遅延リストの例

// Iteratorを使った実装をuseする
// 5.5ならGeneratorImplも使える
use Underbar\IteratorImpl as _;

// 配列の要素を2倍にする($doubleの呼び出しは遅延される)
$xs = _::map([1, 2, 3, 4, 5], $double);
echo _::join($xs);  // 2,4,6,8,10(ここで$doubleが呼ばれる)

// 偶数要素を選択($evenの呼び出しは遅延される)
$even = function($x) {
    // 偶数ならtrue
    return $x % 2 === 0;
};
$ys = _::select([1, 2, 3, 4, 5], $even);
echo _::join($ys);  // 2,4(ここで初めて$evenが呼ばれる)

遅延リストの恩恵

不要な計算をしない

// $doubleは先頭の要素2つ分についてだけ呼び出される
echo _::join(_::take($xs, 2));  // 2,4

空間効率がいい

// 0から10万までの配列を走査
foreach (range(0, 100000) as $x) {
    // 要素の数だけメモリを消費する
}

// 0から10万までの遅延リスト
// (組込みのrange()とは引数の仕様が違います)
foreach (_::range(0, 100001) as $x) {
    // 要素1件分しかメモリを消費しない
}

遅延リストの恩恵

無限リストを作れる

// みんな大好きフィボナッチ数の無限リスト
$fibs = _::chain(array(1, 1))
    ->iterate(function($xs) {
        return array($xs[1], $xs[0] + $xs[1]);
    })
    ->map('Underbar\\IteratorImpl::first')
    ->take(20)
    ->toList();  // フィボナッチ数列を20個取り出す

並列処理もできる

// なんかすごく重い処理
$heayFunc = function($x) {
    sleep(1);
    return $x * 2;
};

// 処理結果を合計する
$result = _::chain(_::range(0, 10))
    ->parMap($heayFunc, 10)
    ->sum()  // 0 + 2 + 4 + 6 + 8 + 10 + 12 + 14 + 16 + 18;

// 1秒強で終わります!
echo $result;  // 90

並列処理(parMap)の実装

Enumerableトレイト

とりあえずクラスを定義

class Collection
{
    // Enumerableをmixinする
    use Underbar\Enumerable;

    protected $array;

    public function __construct()
    {
        // コンストラクターに指定された値をそのまま入れる
        $this->array = func_get_args();
    }
    
    public function getUnderbarImpl()
    {
       // Iteratorを使った実装を使用する
       return "Underbar\\IteratorImpl";
    }

    public function value()
    {
       // Underbarのメソッドの第一引数に渡す値
        return $this->array;
    }
}

Enumerableトレイト

実際に使ってみる

$collection = new Collection(1, 2, 3);

// 2倍にする
$double = $collection
  ->map(function($n) { return $n * 2; })
  ->join($double));

// 要素を循環させる
$cycle = $collection
    ->cycle()  // [1, 2, 3, 1, 2, 3, 1, ...]
    ->map(function($n) { return $n * 2; })
    ->take(6)
    ->join();  // 2,4,6,2,4,6

まとめ