LoginSignup
9
3

More than 5 years have passed since last update.

[PHP]ぼくのかんがえたさいきょうのはいれつ 2016 winter

Last updated at Posted at 2016-12-21

この記事は
PHPで高速オシャレな配列操作を求めて
にインスパイアされたものです。

こんにちは。

皆さん今日も生array使ってますか。私も使ってます。今回はarray的なデータ構造をよりよく扱いたいというテーマで、解決したかった問題についてつらつらと書いていきたいと思います。

結果

https://packagist.org/packages/oubakiou/phpp
https://github.com/oubakiou/phpp

課題1.arrayがArrayオブジェクトじゃない

PHPでも標準で、あるいはPECL等で、array的なデータ構造に対応したクラスが提供されています。

http://php.net/manual/ja/book.ds.php
http://php.net/manual/ja/class.arrayobject.php
http://php.net/manual/ja/spl.datastructures.php

また、ただオブジェクトでありさえすればいいのであれば

(object)[1, 2, 3]

でstdClassへ変換されます。

限界

後述する何れかの問題が解決されないので、クラスを自作したくなるかもしれません。しかしその場合はarrayリテラルによる記述やarrayなら出来る演算子による操作は出来ません。また性能面でもプリミティブなarrayに比べると不利になる事が多いでしょう。

課題2.arrayがミュータブル一択

実行パフォーマンスの話は棚上げにして、基本的にはミュータブルなものを読むよりもイミュータブルなものを読む方が楽です。そして自作のCollectionクラスをイミュータブルな実装にする事は可能です。

限界

PHPにはまだ再代入不可変数が無いので片手落ち感はあります。導入されると良いですね。

課題3.ジェネリクスが欲しい

本題。というわけで自作の道を歩むわけですが

PHPの配列要素の型を縛る

array内要素の型を縛れるジェネリクス的なやつ欲しい、というのを思う人は多いと思います。私もそう思うので実装しました。assertでバリデーションが実行されるので、PHP7以降であれば本番環境では無効化しバリデーションの実行コストをスキップする事が出来ます。

$collection = Arr::ofInts([1, 2, 3]);
$collection = Arr::ofUsers([new User(['name' => 'hoge'])]);

$collection = Arr::ofInts([1, 'hoge', 3]);
// throw TypeError
$collection = Arr::ofUsers([new User(['name' => 'hoge']), 2, 3]);
// throw TypeError

限界

あくまで実行時の動的バリデーションとして実装されています。なので開発時に想定していない実行パスによって実行時バリデーションが失敗する可能性があります。またmapのような操作をするとバリデーション情報が失われるので人力で注入し直す必要があります。(mapへ渡すクロージャの返り値の型によって自動的に解決されたりはしない)

ここらへんはPHPとPhanPythonとmypyのようなジェネリクスがサポートされれば生arrayのままでも綺麗に解決するのかもしれません。また、ちょうどアドベントカレンダーの

PhpStormで手堅く書く

で知った話ですがPHPStormであればそういう感じの事が既に出来るみたいです。エディタ選択の自由がある大人数組織だと少し心許ないかもしれませんが、十分な気はします。

なおofIntsのようなstatic factoryメソッドを設けているのはIDEによる入力補完のためです。JavaがIDEとの組み合わせである種のAlt Javaを実現してるように、PHPであってもIDE(やVim、Emacsプラグイン)の力を借りられる所は借り、半自動入力してもらう事で人の手による温かみのある入力ミスを防いでいきたい。

のですが、例えばAnimalのようなユーザ定義型に対しては動的にバリデーターを生成しているので入力補完が効きません。そこを本気で気にするなら

class AnimalArr extends Arr
{
    public static function of(array $animals)
    {
        $validator = function ($elem) {return instanceof Animal;};
        return new self($animals, '', $validator);
    }
}

new AnimalArr::of($animals);

のようなサブクラスが必要になります。

課題4.array_mapで引数の順番を毎回間違えてIDEに怒られる、一部の配列関数に副作用があって使いにくい、とか

return array_sum(
    array_filter(
        array_map(
            function ($v) {
                return $v ** 2;
            },
            array_filter($range, function ($v) {
                return $v % 2 === 0;
            })
        ),
        function ($v) {
            return $v > 20;
        }
    )
);

より

return Arr::ofInts($range)
    ->filter(function ($v) {
        return $v % 2 === 0;
    })
    ->map(function ($v) {
        return $v ** 2;
    })
    ->filter(function ($v) {
        return $v > 20;
    })
    ->reduceLeft(function ($a, $b) {
        return $a += $b;
    });

の方がいくらか読みやすいですね。前者は中間変数を挟む事でもう少し読みやすくなるので、公平な比較ではないかもしれません。

限界

functionキーワードが書くにも読むにも鬱陶しいというのも、また多くの人が思う所だと思いますが、これは妥協しています。あとreturnキーワードも鬱陶しい。PHP二大書き忘れるキーワードはfunctionとreturnだと思います。

functionキーワードについて妥協していない例
array_mapにありがとう、さよなら

課題5.よりパワフルな関数(メソッド)が欲しい

Underbar.phpのようなUnderscore.jsライクな先駆者やLINQライクな先駆者もいますが、Scalaを勉強中なので

http://seratch.hatenablog.jp/entry/20110429/1304072372
https://github.com/oubakiou/phpp/blob/master/src/Arr.php#L213

をとりあえず実装しました。

  • append(self $that): self
  • head()
  • tail(): self
  • init(): self
  • last()
  • foldLeft(callable $op, $z)
  • foldRight(callable $op, $z)
  • reduceLeft(callable $op)
  • reduceRight(callable $op)
  • foreach(callable $op): void
  • filter(callable $p = null): self
  • filterNot(callable $p): self
  • drop(int $n): self
  • dropWhile(callable $p): self
  • take(int $n): self
  • takeWhile(callable $p): self
  • map(callable $f, string $builtinValidatorName = '', callable $validator = null): self
  • flatMap(callable $f, string $builtinValidatorName = '', callable $validator = null): self
  • flatten(): self
  • collect(callable $p, callable $f): self
  • splitAt(int $n): self
  • slice(int $from, int $untile): self
  • partition(callable $p): self
  • span(callable $p): self
  • groupBy(callable $f) :self
  • unzip(): self
  • find(callable $p)
  • exists(callable $p): bool
  • forall(callable $p): bool
  • count(callable $p = null): int
  • size(): int
  • length(): int
  • min(): int
  • minFloat(): float
  • max(): int
  • maxFloat(): float
  • mkString(string $string1, string $string2 = '', string $string3 = ''): string
  • dropRight($n): self
  • sameElements(self $that): bool
  • zip(self $that): self
  • zipWithIndex(): self
  • apply($key)
  • contains($elem): bool
  • diff(self $that): self
  • startsWith(self $that): bool
  • endsWith(self $that): bool
  • indexOf($elem): int
  • isDefinedAt($key): bool
  • indices(): self
  • distinct(): self
  • reverse(): self
  • reverseMap(callable $f, string $builtinValidatorName = '', callable $validator = null)
  • sorted(): self
  • sortWith(callable $lt): self
  • patch(int $from, self $that, int $replaced)
  • updated(int $n, $elem): self
  • cons($elem): self
  • getOrElse($key, $default = null)
  • keys(): self
  • values(): self

限界

それっぽいというだけで、あんまり挙動を模倣していません。

実用性

とりあえずマイクロベンチマークを取ってみましょう。

ouba-no-MacBook-Air:phpp ouba$ vendor/bin/phpunit tests/ArrBenchTest.php 
PHPUnit 5.7.3 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.1.0
Configuration: /Users/ouba/Desktop/phpp/phpunit.xml

.                                                                   1 / 1 (100%)

bench start

plain          0001 : 75 ms (usage 4MB)
array_function 0002 : 249 ms (usage 4MB)
Arr            0003 : 851 ms (usage 4MB)

bench end



Time: 2.43 seconds, Memory: 6.00MB

OK (1 test, 2 assertions)

plainの10%ぐらいの性能しか出てないですね。大きく息を吸って落ち着いて、本番環境を想定したassert無効のベンチをとってみます。

ouba-no-MacBook-Air:phpp ouba$ php -d zend.assertions=0 `which vendor/bin/phpunit` tests/ArrBenchTest.php 
PHPUnit 5.7.3 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.1.0
Configuration: /Users/ouba/Desktop/phpp/phpunit.xml

.                                                                   1 / 1 (100%)

bench start

plain          0001 : 84 ms (usage 4MB)
array_function 0002 : 239 ms (usage 4MB)
Arr            0003 : 281 ms (usage 4MB)

bench end



Time: 1.22 seconds, Memory: 4.00MB

OK (1 test, 2 assertions)

意外と悪くないのでは?
アンカリング

実用性の話にもどりますが、あまり性能面でシビアな要求がない箇所で、かつ、それによって本当にコードが読みやすくなるのであれば有りな気はします。ただしコード全体での統一性によるリーダビリティ向上と引き換えになるかもしれません。

また動的型付言語であってもリーダビリティが重要な場面において、関数の型宣言はかなり費用対効果が高いと思っていますが、このジェネリクスもどきに関してはちょっと微妙なラインかなと思ってます。

まとめ

多くの職業プログラマの日々における、主な相手であろうドメインの問題と向き合ったコードと、こういったライブラリコードとでは、書いていてまた違った面白さがあります。新しめのPHP機能について勉強になったりと色々と得るものも多いので、皆もさいきょうのはいれつを書いてみるといいんじゃないかなと思いました。(実戦投入は慎重に)

9
3
0

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
9
3