PHP Mentors (Posts tagged functional.programing)

1.5M ratings
277k ratings

See, that’s what the app is perfect for.

Sounds perfect Wahhhh, I don’t wanna

「ドメインモデリングにおける関数型パターン―仕様パターン」を翻訳しました

書籍「実践ドメイン駆動設計 (Object Oriented Selection)」が出版されて、ドメイン駆動設計(DDD)の知名度が上がってきているようです。

そのDDDに関連する分野の1つとして、DSL(ドメイン特化言語)を挙げることができると思います。

デバシッシュ・ゴーシュさん(Debasish Ghosh)は、DSLのエキスパートで、「実践プログラミングDSL」(原題 “DSLs in Action")という本を出されています。この本の第1章のタイトルは、「ドメインの言葉を話す方法を学ぶ」となっています。

デバシッシュさんのブログ記事のうちの1つ、"Functional Patterns in Domain Modeling - The Specification Pattern” に惹かれて、翻訳をしました。翻訳記事の公開について、著者ご本人から快諾頂けたため、以下に掲載させて頂きます。


ドメインモデリングにおける関数型パターン―仕様パターン

原文URL:http://debasishg.blogspot.in/2014/03/functional-patterns-in-domain-modeling.html

2014年3月31日月曜

ドメインをモデリングするときは、ドメインのエンティティと振る舞いをモデル化する。エリック・エヴァンスが書籍「ドメイン駆動設計」で述べたように、焦点を合わせるべきはドメインそれ自体だ。設計、実装したモデルは、ユビキタス言語を語っていなければならない。実装にの都合により、偶有的な複雑性が多々生じても、ドメインの本質を捉えることができるようにするためだ。表現力の豊かなモデルにするには、拡張可能であることも必要である。そして、私たちが拡張性について語るとき、関連する性質として合成性がある。

関数は、オブジェクトよりも自然に合成を行う。そこで、この記事では、ドメイン駆動設計のコアを成すパターンのうちの1つを実装するために、関数型プログラミングのイディオムを用いるー 仕様パターンだ。 仕様パターンで最も多いユースケースは、ドメインのバリデーションを実装することである。エリックのDDD本には、仕様パターンについて次のように書かれている:

仕様の用途は複数あるが、最も基本的な概念を伝えているのは、どんなオブジェクトでも評価して、定義された基準を満たしているかどうかを調べるという使い方である。

仕様は述語として定義され、それによって、業務ルール同士をブール論理を使って互いに連鎖させて結合できるようになる。このように合成の概念があるので、このパターンについて語るときは合成仕様Composite Specification)として語ることができるだろう。DDDにおける各種の文献では、これをCompositeデザインパターンを使って実装しており、ごく一般的にはクラス階層とコンポジションが用いられていた。この記事では、その代わりに関数合成を使う。

仕様はどこにあるのか?

モデルを設計する際によくある問題として、集約ルートやエンティティのバリデーションを行うコードをどこに置くのか、という話がある。

  • エンティティの中でバリデーションをするか? これはダメだ。エンティティが肥大化してしまう。同じエンティティのコアでも、コンテキストによってバリデーションを変えたいことがある。
  • インターフェイスの一部としてバリデーションをするか? JSONを使って、その外側でエンティティを構築するかもしれない。たしかに、ある種のバリデーションはインターフェイスに属すると言えるし、そこにバリデーションを置くことに違和感は無い。
  • しかし、もっとも興味深いバリデーションは、ドメインレイヤーに属するものだ。業務のバリデーション(あるいは仕様)であり、エリック・エヴァンスが他のオブジェクトの状態に関する制約をのべるものと定義している。 業務ルールとして、エンティティを次の処理へ渡す前にバリデーションをかけなければならない。

単純な例で考えよう。注文Order)エンティティとそのモデルがあるとする。新しい注文は、処理工程に入る前に、下記のドメインの「仕様」を満たすものとする。

  1. 妥当valid)な注文でなければならず、ドメインが必要とする正当な日付、正当な商品品目、といった制約に従わなければならない。
  2. 正しい権限で承認approved)されていなければならない。処理工程の次段階に進めるのはその場合だけだ。
  3. 顧客がブラックリストに載っていないことを保証するため、状態を審査しなければならない。
  4. 注文可能かどうかを調べるため、商品品目の在庫を確認しなければならない。

個々の手順に分かれていて、注文の処理工程に沿って順次完了していく。そのようにして、注文の前に、注文実行の用意が整っているどうかを検証する。どこかでエラーがあると、注文は処理工程から外れて、そこで処理が終わる。そして、私たちが設計するモデルは、この順序を知っている必要があるし、手順の一部として経るべき制約はすべて課す必要がある。

注文を変化させるのは手順だけである―ここは重要なポイントだ。すべての仕様は、最初の注文のコピーを入力として受け取り、ドメインルールを検証後、処理工程の次の手順へ進ませて良いかどうか判定する。

実装へ……

私たちがここまでで学んできたことをふまえて、実装に落としこんでみよう。

  • 注文は、少なくとも今回の操作の流れにおいては、不変のエンティティにできる。
  • すべての仕様は1つの注文を必要とする。これがトリックになっていて、仕様に注文インスタンスを順次渡すことで、APIをシンプルに保つことができる。
  • 関数型プログラミングの原則により、上記の処理手順をとしてどのようにモデル化できるか。結果を最後まで合成可能に保ち、注文完了後の次工程へ渡すにはどうするか(次工程については、いずれまた別の記事で議論しよう)。
  • すべての関数は似たシグネチャを持っているようだ―私たちは関数をたがいに合成する必要がある。

あれこれと説明や理論を持ち出すよりも、まずは基本的なビルディングブロックを使ってドメインエキスパートととりまとめた内容を実装していこう。

type ValidationStatus[S] = \/[String, S]

type ReaderTStatus[A, S] = ReaderT[ValidationStatus, A, S]

object ReaderTStatus extends KleisliInstances with KleisliFunctions {
  def apply[A, S](f: A => ValidationStatus[S]): ReaderTStatus[A, S] = kleisli(f)
}

ValidationStatusは、どの関数からも結果として返す型を定義している。それは状態S、または何らかの異常を報告するエラー文字列となる。正確にはscalazで実装されているEither型(右側が正)である。

この実装が優れているのは、すべてのメソッドで注文パラメータが繰り返されることなく処理手順が呼び出されるからだ。そのようにするイディオムの1つとして、Readerモナドが使われる。そして、私たちはすでにモナド―\/はモナドだ―を持っている。そこで、処理結果をモナド変換子を使って積み上げていく。ReaderTがこの作業を受け持つ。結果同士を結びつけてくれるありがたい型としてReaderTStatusを定義する。

次のステップはReaderTStatusの実装で、クライスリと呼ばれる別の抽象を使う。scalazライブラリを使って、クライスリの言葉でReaderTを実装する。実装の詳細に立ち入る話はしないでおく―もし興味があるのであれば、ユージーンによる優れた論文を参照してほしい。

さて、サンプルの仕様はどのようになるのか?

話に入る前に、基本的な抽象を用意する(わかりやすさのため、ごく単純にしてある)。

// 基底の抽象
sealed trait Item {
  def itemCode: String
}

// サンプル実装
case class ItemA(itemCode: String, desc: Option[String],
  minPurchaseUnit: Int) extends Item
case class ItemB(itemCode: String, desc: Option[String],
  nutritionInfo: String) extends Item

case class LineItem(item: Item, quantity: Int)

case class Customer(custId: String, name: String, category: Int)

// 注文のスケルトン
case class Order(orderNo: String, orderDate: Date, customer: Customer,
  lineItems: List[LineItem])

そして、下記が注文オブジェクトの制約を検査する仕様である。

// 基本的なバリデーション
private def validate = ReaderTStatus[Order, Boolean] {order =>
  if (order.lineItems isEmpty) left(s"Validation failed for order $order")
  else right(true)
}

これは説明用の例に過ぎないので、ドメインルールが多く含まれているわけではない。 重要なのは、関数を実装するために定義した型をどう使っているかだ。 注文は関数に対する暗黙の引数ではない―それはカリー化される。関数はReaderTStatusを返す。ReaderTStatus自体はモナドである。したがって、別の仕様と併せて処理工程を手順化していくことができる。つまり、決められた順序の要求を、式指向プログラミングの枠組みの中で解決できるのだ。

収集したドメイン知識に基づく仕様はほかにもあった。

private def approve = ReaderTStatus[Order, Boolean] {order =>
  right(true)
}

private def checkCustomerStatus(customer: Customer) = ReaderTStatus[Order, Boolean] {order =>
  right(true)
}

private def checkInventory = ReaderTStatus[Order, Boolean] {order =>
  right(true)
}

これらを互いにつなぐこと

しかし、ドメインが与える操作順序を表すには、これらのピースをどうつなげば良いのだろう。モデルで合成性による利点を享受するにはどうするのか? これは非常に簡単だ。合成に適した型定義という難しい仕事はすでに終わっている。

以下のisReadyForFulfilmentメソッドでは「合成仕様」を定義していて、for内包表記(for-comprehension)でくるまれた個々の仕様すべてを順番に呼び出している。

def isReadyForFulfilment(order: Order) = {
  val s = for {
    _ <- validate
    _ <- approve
    _ <- checkCustomerStatus(order.customer)
    c <- checkInventory
  } yield c
  s(order)
}

このように、モナディックな結合により、順序立てられた一連の処理を、抽象の合成性を保ちつつ実装することができる。次回は、注文の後工程を合成可能にする方法、エンティティの情報を読むだけでなく可変とするやり方についても見ていこう。もちろん、関数型のアプローチで。


翻訳後記

この記事は、初学者の私が、背伸びをして勉強しながら訳したものです。推敲を重ねましたが、誤訳等が含まれている可能性はあります。もし何かありましたら、ご指摘頂けると幸いです。

関連記事

参考

ddd dsl practical.ddd functional.programing domain.modeling modeling analysis

PHPにおける宣言的集合操作入門:Ginq

配列、連想配列といったデータの集まり - 集合に対する操作は、日々のプログラミングにおいて頻繁に記述するコードの1つです。その一方で、旧来の愚直なループを使った集合操作はコードを複雑にする大きな要因となります。これに対処するために、Microsoftは統合言語クエリ:LINQ(Language-Integrated Query)を開発しました。LINQ to Objectsのページには、LINQを使うメリットとして次のように説明があります。

本質的に、LINQ to Objects は、コレクションを扱うための新しい方法です。 従来の方法では、複雑な foreach ループを記述して、コレクションからどのようにデータを取得するかを指定する必要がありました。 LINQ を使用する場合は、何を取得するかを表す宣言コードを記述します。 また、LINQ クエリには、従来の foreach ループと比べて次の 3 つの重要な利点があります。 簡潔で読みやすい (特に複数の条件をフィルター処理する場合)。 強力なフィルター処理、並べ替え、およびグループ化機能を最小限のアプリケーション コードで実現できる。 ほとんど変更せずに、他のデータ ソースに移植できる。 一般に、データに対して実行する操作が複雑なほど、従来の反復処理の代わりに LINQ を使用することの意義が高まります。 LINQ to Objects (強調は引用者による)

PHPにはarray_filter()array_map()array_reduce()といった関数がPHP4の頃から用意されており、集合に対する操作を宣言的にコーディングすること自体は部分的に可能でした。これらの関数にはインターフェイスにバラつきがあり、なかなか使いづらいという難点もありました。

ライブラリGinqを使うと、LINQ to Objectsの提供するメソッド群(標準クエリ演算子)に近いAPIで操作できます。Ginqを使うとどのようなコードになるのか、いくつかの例で紹介します。

コード比較

以下のコードはGitHubで公開しています。

まずはbleis-tiftさんの記事よくあるコーディングパターンと LINQ to Objects の対応付けにある例のいくつかをPHP、PHPのarray_*関数、Ginqを使ったコードで比較してみます。それぞれのコード片で2つまたは3つのメソッドを次のように作成しています。

  • normal():素のPHPで書いた例(foreachでループする)
  • normal2():(可能であれば)PHPのarray_*系関数を使った例
  • ginq():Ginqを使った例

射影: select その1

最初は、(数値)配列のすべての要素を2倍する例です。要素の変換処理にあたり、Ginqではselect()を使います。この内容であれば、array_map()でもGinqと同じように書けます。

select()には「セレクタ(selector)」をクロージャで指定します。

/**
 * すべての要素を2倍する
 */
class DoubleAll
{
    /**
     * @param $data
     * @return array
     */
    public function normal($data)
    {
        $result = [];
        for ($i = 0; $i < count($data); $i++) {
            $result[$i] = $data[$i] * 2;
        }

        return $result;
    }

    /**
     * @param $data
     * @return array
     */
    public function normal2($data)
    {
        return array_map(function ($v) {
            return $v * 2;
        }, $data);
    }

    /**
     * @param Ginq $data
     * @return mixed
     */
    public function ginq($data)
    {
        return $data->select(function ($v) {
            return $v * 2;
        });
    }
}

次のようなテストコードを用意して確認します。Ginqについては例を分かりやすくするために呼び出し側でGinqオブジェクトの準備と、戻り値の配列化を行っています。

class DoubleAllTest extends \PHPUnit_Framework_TestCase
{
    /**
     * @var DoubleAll
     */
    private $SUT;

    /**
     * @test
     */
    public function DoubleAll()
    {
        $data =     [1, 2, 4,  5, 10];
        $expected = [2, 4, 8, 10, 20];

        $this->assertThat($this->SUT->normal($data), $this->equalTo($expected));
        $this->assertThat($this->SUT->normal2($data), $this->equalTo($expected));
        $this->assertThat($this->SUT->ginq(Ginq::from($data))->toArray(), $this->equalTo($expected));
    }

    protected function setUp()
    {
        $this->SUT = new DoubleAll();
    }
}

射影: select その2

array_map()では同時に処理する配列を複数指定できるので、これを使って配列のインデックスをコールバックに渡せます。次の例でこのパターンを使っています。

/**
 * インデックスが偶数の要素のみ2倍する
 */
class DoubleIfEvenIndex
{
    public function normal($data)
    {
        $result = [];
        for ($i = 0; $i < count($data); $i++) {
            if ($i % 2 == 0) {
                $result[$i] = $data[$i] * 2;
            } else {
                $result[$i] = $data[$i];
            }
        }

        return $result;
    }

    public function normal2($data)
    {
        return array_map(function ($v, $k) {
            return ($k % 2 == 0) ? $v * 2 : $v;
        }, $data, array_keys($data));
    }

    public function ginq($data)
    {
        return $data->select(function ($v, $k) {
            return ($k % 2 == 0) ? $v * 2 : $v;
        });
    }
}

選択: where その1

次は要素を選択する例です。Ginqではwhere()を使います。PHPの関数としてはarray_filter()が使えます。

where()には「述語(predicate)」をクロージャで指定します。

/**
 * 奇数のみを取り出す
 */
class FilterOdd
{
    public function normal($data)
    {
        $result = [];
        for ($i = 0; $i < count($data); $i++) {
            if ($data[$i] % 2 != 0) {
                $result[] = $data[$i];
            }
        }

        return $result;
    }

    public function normal2($data)
    {
        return array_filter($data, function($v) {
            return $v % 2 == 1;
        });
    }

    public function ginq($data)
    {
        return $data->where(function ($v) {
            return $v % 2 == 1;
        });
    }
}

選択: where その2

前の例くらいまでならPHPのarray_map()、array_filter()でも記述量はさほど変わらないように見えます。しかし次の例の場合、array_map()とは異なり、array_filter()のコールバックに配列のインデックスを渡せないので、困ったことになります。Ginqであればwhere()のコールバックでインデックスを評価するだけです。

(PHPの関数だけで何とかする方法に興味のある方は参考のリンク先を参照してください。)

/**
 * インデックスが奇数の要素のみを取り出す
 */
class FilterOddIndexElem
{
    public function normal($data)
    {
        $result = [];
        for ($i = 0; $i < count($data); $i++) {
            if ($i % 2 != 0) {
                $result[] = $data[$i];
            }
        }

        return $result;
    }

    /** array_filter() では素直には書けない... ★参考★ */

    public function ginq($data)
    {
        return $data->where(function ($v, $k) {
            return $k % 2 == 1;
        });
    }
}

量指定子: all

all()は、集合のすべての要素が条件を満たすかどうかを調べるのに使います。

use Stringy\StaticStringy as String;

/**
 * すべての要素が .txt で終わるかどうか調べる
 */
class All
{
    public function normal($data)
    {
        foreach ($data as $element) {
            $endStr = mb_substr($element, mb_strlen($element) - 4);
            if ($endStr != '.txt') {
                return false;
            }
        }

        return true;
    }

    public function normal2($data)
    {
        return array_reduce($data, function ($temp, $v) {
            return $temp & String::endsWith($v, '.txt');
        }, true);
    }

    public function ginq($data)
    {
        return $data->all(function ($v) {
            return String::endsWith($v, '.txt');
        });
    }
}

量指定子: any

any()はall()と似ていますが、集合の要素のうちどれか1つでも条件を満たすかどうかを調べるのに使います。

use Stringy\StaticStringy as String;

/**
 * .txt で終わる要素が1つでもあるかどうか調べる
 */
class Any
{
    public function normal($data)
    {
        foreach ($data as $element) {
            $endStr = mb_substr($element, mb_strlen($element) - 4);
            if ($endStr == '.txt') {
                return true;
            }
        }

        return false;
    }

    public function normal2($data)
    {
        // 計算量の無駄はある・・・
        return array_reduce($data, function ($temp, $v) {
            return $temp | String::endsWith($v, '.txt');
        }, false);
    }

    public function ginq($data)
    {
        return $data->any(function ($v) {
            return String::endsWith($v, '.txt');
        });
    }
}

量指定子: contains

contains()は、集合に指定した要素が含まれるかどうかを調べるのに使います。

/**
 * 要素に hoge を含むかどうか調べる
 */
class Contains
{
    public function normal($data)
    {
        return in_array('hoge', $data);
    }

    public function ginq($data)
    {
        return $data->contains('hoge');
    }
}

集計操作: count

集計も行えます。集合内で特定の条件にマッチする要素の数を調べる場合にcount()を使います。このような集計処理ではarray_reduce()関数も使えます。

/**
 * 偶数の要素の個数を調べる
 */
class Count
{
    public function normal($data)
    {
        $result = 0;
        foreach ($data as $element) {
            if ($element % 2 == 0) {
                $result++;
            }
        }

        return $result;
    }

    public function normal2($data)
    {
        return array_reduce($data, function ($temp, $v) {
            return ($v % 2 == 0) ? $temp + 1: $temp;
        }, 0);
    }

    public function ginq($data)
    {
        return $data->count(function ($v) {
            return $v % 2 == 0;
        });
    }
}

パーティショニング: skip

集合から部分集合を取り出します。skipは、順に並んだ要素の先頭部分を捨てて残り部分の集合を返します。Ginqではdrop()になっています。

/**
 * 先頭10要素を捨てる
 */
class Skip
{
    public function normal($data)
    {
        return array_slice($data, 10);
    }

    public function ginq($data)
    {
        return $data->drop(10);
    }
}

パーティショニング: skipWhile

skipWhileでは、集合の先頭から、条件を満たさない要素をすべて捨てます。GinqではdropWhile()になっています。

/**
 * 先頭から条件を満たす要素以前の要素を捨てる
 * (空文字列またはnullなら捨てる)
 */
class SkipWhile
{
    public function normal($data)
    {
        for ($i = 0; $i < count($data); $i++) {
            if (!empty($data[$i])) {
                break;
            }
        }

        return array_slice($data, $i);
    }

    public function ginq($data)
    {
        return $data->dropWhile(function ($v) {
            return empty($v);
        });
    }
}

パーティショニング: take

take()はskipの逆で、先頭から指定個数の要素だけ取り出します。

/**
 * 先頭10要素を取得
 */
class Take
{
    public function normal($data)
    {
        return array_slice($data, 0, 10);
    }

    public function ginq($data)
    {
        return $data->take(10);
    }
}

パーティショニング: takeWhile

takeWhile()では、集合の先頭から条件を満たす要素だけを取り出します。

/**
 * 先頭からnullまでの要素を取得
 */
class TakeWhile
{
    public function normal($data)
    {
        for ($i = 0; $i < count($data); $i++) {
            if ($data[$i] === null) {
                break;
            }
        }

        return array_slice($data, 0, $i);
    }

    public function ginq($data)
    {
        return $data->takeWhile(function ($v) {
            return $v !== null;
        });
    }
}

要素操作: first

集合の中で特定の条件を満たす最初の要素を取得するのにfirst()を使います。

/**
 * 空でもnullでもない最初の要素
 */
class First
{
    public function normal($data)
    {
        foreach ($data as $value) {
            if (!empty($value)) {
                return $value;
            }
        }
    }

    public function ginq($data)
    {
        return $data->first(function ($v) {
            return !empty($v);
        });
    }
}

上記以外にも様々な機能があります。何らかのキーでグループ化するのはループ処理ではちょっと大変ですが、GinqにはgroupBy()やtoLookup()といったメソッドが用意されていて手軽にできます。

グループ化: toLookup その1

以降はGinqのコード例のみ示します。複数の従業員情報を部署ごとにグループ化し、部署名をキーとした連想配列を取得しています。

class Emploee
{
    public $name;
    public $department;
    public $salary;

    function __construct($name, $department, $salary)
    {
        $this->name = $name;
        $this->department = $department;
        $this->salary = $salary;
    }
}

class ToLookupTest extends TestCase
{
    /**
     * 部署でグループ
     * @test
     */
    public function ToLookup()
    {
        $employees = [];
        $employees[] = new Emploee('あいう', '部署1', 1000);
        $employees[] = new Emploee('かきく', '部署2', 1200);
        $employees[] = new Emploee('さしす', '部署1', 800);
        $employees[] = new Emploee('たちつ', '部署3', 900);
        $employees[] = new Emploee('なにぬ', '部署2', 1000);
        $employees[] = new Emploee('はひふ', '部署4', 700);
        $employees[] = new Emploee('まみむ', '部署2', 900);

        $data = Ginq::from($employees);

        $result = $data->toLookup(
            function ($v) {return $v->department;},
            function ($v) {return [$v->name, $v->salary];}
        )->toArrayRec();

        $this->assertThat($result, $this->equalTo([
            '部署1' => [
                ['あいう', 1000,],
                ['さしす', 800,],
            ],
            '部署2' => [
                ['かきく', 1200,],
                ['なにぬ', 1000,],
                ['まみむ', 900,],
            ],
            '部署3' => [
                ['たちつ', 900,],
            ],
            '部署4' => [
                ['はひふ', 700,],
            ],
        ]));
    }
}

グループ化: toLookup その2

同じデータで、時給をキーにグループ化しています。取得後の要素を並べ替えるためのorderBy()、thenBy()も指定しています。

class Emploee
{
    public $name;
    public $department;
    public $salary;

    function __construct($name, $department, $salary)
    {
        $this->name = $name;
        $this->department = $department;
        $this->salary = $salary;
    }
}

class ToLookupTest extends TestCase
{
    /**
     * 時給でグループ
     * @test
     */
    public function ToLookup2()
    {
        $employees = [];
        $employees[] = new Emploee('あいう', '部署1', 1000);
        $employees[] = new Emploee('かきく', '部署2', 1200);
        $employees[] = new Emploee('さしす', '部署1', 800);
        $employees[] = new Emploee('たちつ', '部署3', 900);
        $employees[] = new Emploee('なにぬ', '部署2', 1000);
        $employees[] = new Emploee('はひふ', '部署4', 700);
        $employees[] = new Emploee('まみむ', '部署2', 900);

        $data = Ginq::from($employees);

        $result = $data
            ->orderBy('salary')
            ->thenBy('name')
            ->toLookup(
                function ($v) {return $v->salary;},
                function ($v) {return [$v->name];}
            )->toArrayRec();

        $this->assertThat($result, $this->equalTo([
            700 => [
                ['はひふ',],
            ],
            800 => [
                ['さしす',],
            ],
            900 => [
                ['たちつ',],
                ['まみむ',],
            ],
            1000 => [
                ['あいう',],
                ['なにぬ',],
            ],
            1200 => [
                ['かきく',],
            ],
        ]));
    }
}

グループ化: toLookup その3

同じデータで、処理を2段階で行って取得後の連想配列を加工しています(2段階目はmap()なので特別な機能というわけではありませんが)。

class Emploee
{
    public $name;
    public $department;
    public $salary;

    function __construct($name, $department, $salary)
    {
        $this->name = $name;
        $this->department = $department;
        $this->salary = $salary;
    }
}

class ToLookupTest extends TestCase
{
    /**
     * 時給でグループ
     * @test
     */
    public function ToLookup2()
    {
        $employees = [];
        $employees[] = new Emploee('あいう', '部署1', 1000);
        $employees[] = new Emploee('かきく', '部署2', 1200);
        $employees[] = new Emploee('さしす', '部署1', 800);
        $employees[] = new Emploee('たちつ', '部署3', 900);
        $employees[] = new Emploee('なにぬ', '部署2', 1000);
        $employees[] = new Emploee('はひふ', '部署4', 700);
        $employees[] = new Emploee('まみむ', '部署2', 900);

        $data = Ginq::from($employees);

        $result = $data
            ->orderBy('salary')
            ->thenBy('name')
            ->toLookup(
                function ($v) {return $v->salary;},
                function ($v) {return ['name' => $v->name];}
            )
            ->toArrayRec();

        $data2 = Ginq::from($result);

        $result2 = $data2->map(
                function ($v) {
                    $temp = [];
                    $temp['list'] = $v;
                    $temp['count'] = count($temp['list']);

                    return $temp;
                }
            )
            ->toArrayRec();

        $this->assertThat($result2, $this->equalTo([
            700 => [
                'list' => [
                    [ 'name' => 'はひふ',],
                ],
                'count' => 1,
            ],
            800 => [
                'list' => [
                    ['name' => 'さしす',],
                ],
                'count' => 1,
            ],
            900 => [
                'list' =>
                [
                    ['name' => 'たちつ',],
                    ['name' => 'まみむ',],
                ],
                'count' => 2,
            ],
            1000 => [
                'list' =>
                [
                    ['name' => 'あいう',],
                    ['name' => 'なにぬ',],
                ],
                'count' => 2,
            ],
            1200 => [
                'list' =>
                [
                    ['name' => 'かきく',],
                ],
                'count' => 1,
            ],
        ]));
    }
}

まとめ

Ginqのようなライブラリを導入することで、集合操作に対する統一されたAPIを、PHPにおいてもインフラとして使えるようになります。

また、宣言的に定義された操作は高階関数なので、汎用的に使ったり、合成することもできます。コレクションに対する条件を部品として切り出すのに使うこともできます。このように作られた部品はドメインの知識を反映した述語となり、ユビキタス言語で集合を操作するよう端的にコードを記述できるようになります。

forやforeachから卒業するために、まずはここで紹介したような簡単な例から練習してみるとよいでしょう。

参考

LINQ to Objects 標準クエリ演算子のマニュアル

php functional.programing ginq