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

昔、こんな記事を書いたことがあるのですが、
[PHPで高速オシャレな配列操作を求めて - Qiita](http://qiita.com/Hiraku/items/190443b33ee7a2167ade)

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

[spindle/spindle-collection: The fastest php collection library](https://github.com/spindle/spindle-collection)

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


## 使い方

```bash
$ composer require spindle/collection
```

```php
<?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`

## 短い記法

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

```php
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の場合、式を文字列で渡すとそれっぽく評価してくれます。
要素は`$_`として受け取ることができます。

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

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

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


## 速さ

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

* spindle-range ... spindle/collectionの普通の書き方
* spindle-range-closure ... spindle/collectionでクロージャで記述
* cakephp/collection ... https://github.com/cakephp/collection
* illuminate/support ... https://github.com/illuminate/support
* native-for ... 温かみのある手書きfor文
* native-array ... array_xxxで頑張って記述

<img width="713" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/1126/1c8c776c-f5d0-e143-3583-9c7c157132b0.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
<?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系とか特に)