Posted at

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

More than 1 year has passed since last update.

昔、こんな記事を書いたことがあるのですが、

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系とか特に)