LoginSignup
42
25

More than 5 years have passed since last update.

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

Posted at

昔、こんな記事を書いたことがあるのですが、
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系とか特に)
42
25
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
42
25