PHPの高速オシャレ危険配列ライブラリspindle/collectionを作っている話

  • 33
    いいね
  • 2
    コメント

昔、こんな記事を書いたことがあるのですが、
PHPで高速オシャレな配列操作を求めて - Qiita

やっぱりforeachを書きまくってて疲れてきたので、コレクションライブラリとして仕立てました。

spindle/spindle-collection: The fastest php collection library

※割とAPIに悩んでいるので、まだしばらくは0.X系にしておくつもり。0.0.2までは無かったことにして下さい。

使い方

$ composer require spindle/collection
<?php
require 'vendor/autoload.php';

use Spindle\Collection\Collection;

(new Collection(range(1, 100))
    ->filter('$_ % 10 === 0')
    ->map('$_ * 2')
    ->assignTo($collection);

echo $collection->join(', ');
//20, 40, 60, 80, 100, 120, 140, 160, 180, 200

キーワード

  • evalこそパワー(危険だが高速)
  • 短い記法
    • もちろんクロージャでも書けるよ
  • 代入をひっくり返して書ける終端assignToメソッド
  • 手書きforeachの次に高速
    • laravelのIlluminate\Support\Collectionの2倍弱速い
    • CakePHP3のCake\Collection\Collectionの4倍速い
  • 個人的に欲しい機能を入れる
    • 安定ソート版のusort usortStable
    • シュワルツ変換つきソート mapSort
    • N+1問題対策でよく書く leftJoin

短い記法

よくあるコレクションライブラリはクロージャを使って書きますので、こんな見た目になります。

use Cake\Collection\Collection;
echo (new Collection(range(1, 1000)))
    ->map(function($v){ return $v * 2; })
    ->filter(function($v) { return $v % 3 === 0; })
    ->sumOf();

spindle/collectionの場合、式を文字列で渡すとそれっぽく評価してくれます。
要素は$_として受け取ることができます。

use Spindle\Collection\Collection;
Collection::range(1, 1000)
    ->map('$_ * 2')
    ->filter('$_ % 3 === 0')
    ->assignTo($col);
echo $col->sum();

もちろん、普通のクロージャや関数名などのcallableを渡すこともでき、これも動作します。

use Spindle\Collection\Collection;
Collection::range(1, 1000)
    ->map(function($_) { return $_ * 2; })
    ->filter(function($_) { return $_ % 3 === 0; })
    ->assignTo($col);
echo $col->sum();

速さ

適当なベンチマークで実行時間測定してみました。実行時間だから棒が短いほうが速い。
(あとでリポジトリ公開予定)

image.png

流石に生で書いたfor文には及びませんが、他のコレクションライブラリよりは高速に処理できています。

これは、ソースコードを生成して最後に1回eval()して実行しているためです。
->filter()->map() の度にループを回さないし、Iteratorによる都度メソッド実行のオーバーヘッドもありません。その分高速に処理できるわけです。

懸念

  • 素直にevalしているので、ユーザー入力値をそのままmapなどに渡したら即死レベルのセキュリティの穴になる
  • evalはopcacheが効かないので、Webアプリケーションにおけるパフォーマンスも測定した方が良さそう。

API抜粋

ライブラリと銘打ってますが、Spindle\Collection\Collection というクラスが一個あるだけです。
配列やTraversableなオブジェクトをこのクラスに変換すると、map/reduceといったメソッドが使えるようになります。

大抵のメソッドは自分自身を返すため、メソッドチェーンでコードを組み立てていくことができます。

オブジェクトの生成

$seedとして配列や、Traversableなオブジェクトを渡してnewしてください。

(new \Spindle\Collection\Collection($seed))

クラス名がPSR-4仕様で長ったらしいので、適当にuse文を書いておくと便利です。
直接newする代わりに、staticメソッドがいくつか使えます。

<?php
use Spindle\Collection\Collection;

$col = new Collection(range(1, 10));

// newと同じ意味、メソッドチェーンする時はカッコが一つ外せるので楽
$col = Collection::from(range(1, 10));

// 1 ~ 10の連番を作る。ジェネレータ式なのでメモリに優しい
$col = Collection::range(1, 10);
// 文字列も対応。
$col = Collection::range('a', 'z');

// 指定回数繰り返し同じ要素を返す
$col = Collection::repeat('yes', 10);

要素を変換する系

->map($fn)
一要素ごとに加工します。$fnは引数1個です。

->mapWithKey($fn)
$fnに渡される引数が(key, value)になります。

->column($columns)
->flip()

集合操作系

->chunk($size)
->groupBy($fn)
->leftJoin($map, callable $fetch, callable $combine)

ソート系

全件見ないといけないので、配列化されます。seedが無限長だとうまく動きません。

sort, rsort, usort, usortStable, mapSort

(今後拡張予定)

終端系

計算結果を返すので、メソッドチェーンが途切れます。

->join($separator)
全要素を文字列として連結した結果を返します。要するにimplodeです。

->sum()
全要素が数字でないと成功しません。足し合わせた結果を返します。

->product()
全要素が数字でないと成功しません。掛け合わせた結果を返します。

->count()
数を数えます。

->toArray()
普通のPHPの連想配列にして返します。

->reduce($fn, $_carry=null)
いわゆるreduceです。

今後の展望

  • APIの拡充(sort系とか特に)