Laravel/Collection 勉強会資料 関数型プログラミングについて語る

LaravelPHP関数型プログラミング

出典: 

3/25勉強会資料

  • 使い方
  • 使い方はリファレンス読めばいいだけなので、背景や嬉しいことをまず説明します

そもそもCollectionって何

Illuminate\Support\Collectionクラスは  
配列データを操作するための、書きやすく使いやすいラッパーです。
(中略)
メソッドをチェーンでスムーズにつなげてくれます。  
つまり元のコレクションは不変であり、  
全てのCollectionメソッドは新しいCollectionインスタンスを返します。
  • Laravelが提供する、「賢いarray
  • 配列データ操作が幸せになる
  • \DB::table('users')->get();とかで返ってきています

配列データ操作の比較

  • 例題

    [1, 2, 3, 4, 5]の各要素を2乗して、10未満のもののみ抽出し、総和をとれ

答え: 14


[1, 2, 3, 4, 5]

[1, 4, 9, 16, 25]

[1, 4, 9]

1 + 4 + 9 = 14

- 同じデータ操作処理をいろいろな方法で書いてみる
    - 手続き型
    - 関数型 (PHP組み込み)
    - オブジェクト指向 + 関数型 (`Collection`クラス)
    - 真の関数型


## 手続き型

### for文使う例

```php
<?php

$arr = [1, 2, 3, 4, 5];
$len = count($arr);
$sum = 0;

for ($i = 0; $i < $len; $i++) {
    $arr[$i] = $arr[$i] * $arr[$i];
    if ($arr[$i] >= 10) {
        continue;
    }
    $sum += $arr[$i];
}

var_dump($sum);
  • foreach使え

    • やりたいことは「全要素についてxxする」
    • $lenとか$iとかは関心の対象ではない。目障り
  • $arrのメンバを変更してしまっている

    • 処理前: $arr[1, 2, 3, 4, 5]
    • 処理後: $arr[1, 4, 9, 16, 25]
    • 他で$arrを使う場合やばい
  • 元の値を変更しない: 不変であるという

「わちゃっ」としている例

<?php

$arr = [1, 2, 3, 4, 5];
$sum = 0;

foreach ($arr as $value) {
    $squared = $value * $value;
    if ($squared >= 10) {
        continue;
    }
    $sum += $squared;
}

var_dump($sum);
  • ぱっと見、何してるのかよくわからない

ステップ分けた例

<?php

$arr = [1, 2, 3, 4, 5];
$filteredArr = [];
$sum = 0;

foreach ($arr as $value) {
    $squaredArr[] = $value * $value;
}

foreach ($squaredArr as $squared) {
    if ($squared < 10) {
        $filteredArr[] = $squared;
    }
}

foreach ($filteredArr as $filtered) {
    $sum += $filtered;
}

var_dump($sum);
  • 一時変数まみれ
  • 一時変数の宣言と、定義・利用箇所が遠くなっていきがち

    • 別の処理が間に挟まれたりして、いつの間にか手に負えないコードになる

関数型 (PHP組み込み)

高階関数

  • 関数を引数にとる関数
  • 「arrayの各要素について『何かする』」関数

    • 『何かする』部分を関数として渡す
  • 一時変数や、配列のインデックス$iが出てこないのが嬉しい

array_map

  • 配列の各要素に関数を作用させて、新しい配列を作る
<?php

$arr = [1,2,3,4,5];

$mapped = array_map(
    function ($val) { return $val * $val; },
    $arr
);

var_dump($mapped);
array(5) {
  [0]=>
  int(1)
  [1]=>
  int(4)
  [2]=>
  int(9)
  [3]=>
  int(16)
  [4]=>
  int(25)
}

array_reduce

  • 配列を2項関数で再帰的に縮約する(左結合)
<?php

$arr = [1,2,3,4,5];

$reduced = array_reduce(
    $arr,
    function ($a, $b) { return "($a + $b)"; },
    0
);

var_dump($reduced);

$reduced2 = array_reduce(
    $arr,
    function ($a, $b) { return $a + $b; },
    0
);

var_dump($reduced2);
string(31) "(((((0 + 1) + 2) + 3) + 4) + 5)"
int(15)

array_filter

  • 配列の各要素に関数を適用し、trueになる要素のみ抽出して、新しい配列を作る

    • true/falseを返す判定関数を「述語関数」(predicate)といいます
<?php

$arr = [1,2,3,4,5];

$filtered = array_filter(
    $arr,
    // 2で割ったあまりが0 : 偶数
    function ($val) { return $val % 2 === 0; }
);

var_dump($filtered);
array(2) {
  [1]=>
  int(2)
  [3]=>
  int(4)
}

実装例

一時変数あり

<?php

$arr = [1, 2, 3, 4, 5];

$squaredArr = array_map(
    function ($v) { return $v * $v; },
    $arr
);

$filteredArr = array_filter(
    $squaredArr,
    function ($v) { return $v < 10; }
);

$calced = array_reduce(
    $filteredArr,
    function ($a, $b) { return $a + $b; }
);

var_dump($calced);
  • good

    • ルーブ文が出てこない

      • やりたいこと「全要素をxxする」をダイレクトに表現できている
      • 「宣言的プログラミング」という
  • bad

    • 一時変数まみれ。バグの温床

      • 宣言と定義が同時なので手続き型よりはマシ

一時変数なし

<?php

$arr = [1, 2, 3, 4, 5];

$calced = array_reduce(
    array_filter(
        array_map(
            function ($v) { return $v * $v; },
            $arr
        ),
        function ($v) { return $v < 10; }
    ),
    function ($a, $b) { return $a + $b; }
);

var_dump($calced);
  • good

    • 一時変数消えた
  • bad

    • 破滅のピラミッド

      • インデントが横倒しのピラミッドになる
    • 書いた逆順(内側から外側)に処理されるのがつらい

その他、PHP組み込みのarray操作関数のつらみ

  • 引数の順番に統一性がない

    • array_mapはzipを兼ねるから配列が最後なのだろうか
  • 一時変数か、破滅のピラミッドか

オブジェクト指向 + 関数型 (Collectionクラス)

<?php

$arr = [1, 2, 3, 4, 5];

$calced = collect($arr)
    ->map(function ($v) { return $v * $v; })
    ->filter(function ($v) { return $v < 10; })
    ->reduce(function ($a, $b) { return $a + $b; });

var_dump($calced);
int(14)
  • やりたいことをダイレクトに表現できる!
  • 一時変数ない!
  • 処理順に書ける!
  • インデント深くならない!

【補】真の関数型

  • 下記、雰囲気のコードです。動きません
<?php

$arr = [1, 2, 3, 4, 5];

// 関数を合成して新しい関数を作る。
// 各要素を2乗して、10未満のもののみ拾い、総和を取る関数
$calcFunc = $pipe(
    $map(function ($v) { return $v * $v; }),
    $filter(function ($v) { return $v < 10; }),
    $reduce(function ($a, $b) { return $a + $b; })
);

$calced = $calcFunc($arr);

var_dump($calced);
  • $pipeは関数を合成する関数
    数学的に表現するとこういう気持ち
pipe(f, g, h)(x) := h(g(f(x)))
pipe(f, g, h) := h・g・f  ... Point-Free Style
  • $map$filter$reduceメソッドではなく関数
  • なので、処理対象データはCollection等、特定のインスタンスに依存しない

    • 自前の線形リストクラスList等を作っても問題なく適用できる
  • 周りに影響を及ぼすことなくデバッグコードを仕込みやすい
<?php

$trace = function ($val) {
    var_dump($val);
    return $val;
};

$calcFunc = $pipe(
    $trace, // 入力を表示
    $map(function ($v) { return $v * $v; }),
    $trace, // 2乗後表示
    $map($trace), // 要素別表示
    $filter(function ($v) { return $v < 10; }),
    $trace, // 10未満のみ抽出したもの表示
    $reduce(function ($a, $b) { return $a + $b; })
);

各手法の特徴

手続き型 関数型
(PHP組み込み)
Collection
オブジェクト指向
+
関数型
真の関数型
不変 o o o
宣言的 x o o o
一時変数地獄 x o o
破滅のピラミッド o x o o
汎用性・拡張性 o x?
あまり知らない
x o
デバッグコード
入れやすい
x x
ピラミッド深くなる
o o
テスト容易 x o o o
性能 o x x x
記述量 o o o x
何かライブラリが
あれば別
  • テスト容易性

    • 関数型一般に高い
    • 高階関数に渡す関数は、小さく、単純

      • $add = function ($a, $b) { return $a + $b; };
      • 純粋関数

        • 戻り値が引数によって完全に決まるやつ
      • テストしやすい

        • assert(3 === $add(1, 2));
  • 性能

    • 関数型一般に、手続き型には劣るはず

      • 外から見えないだけで、中でループが走っている
      • ループのたびに、引数で渡した関数が呼び出されている
      • 関数呼び出しにはコストがかかる
    • 推測よりも計測

      • ボトルネックになっていなければ、気にしなくて良いのでは?
      • 読みやすく、書きやすい方法で開発・メンテコストを下げる
  • 記述量

    • 関数型ライブラリがなければ自作しないといけない

      • $map関数とか
    • 関数型ライブラリがあったらあったで、自前オブジェクトをライブラリに対応させるにはコーディングが必要

      • $map関数に対応させるために、自前のListクラスにList@mapメソッド生やすとか
  • その他

    • オブジェクト指向言語で真の関数型プログラミングを追い求めることに無理がある

      • シングルディスパッチしかできない
      • パターンマッチが無い

結論

  • Collectionがよい落とし所かなと思います

Collectionの嬉しさがわかったところで… -> 使い方のお勉強

  • よく使いそうなやつ
  • わかりやすいやつ

おまけ

Michael Feathers on Twitter

OO makes code understandable by encapsulating moving parts.
FP makes code understandable by minimizing moving parts.

オブジェクト指向は可動部をカプセル化することでコードを理解しやすくする。
関数型プログラミングは可動部を極力減らすことでコードを理解しやすくする。

  • 「可動部」 = 状態(state)のことでしょう

    • 局所変数

      • 一時変数
      • 関数の引数
    • メンバ変数
  • 一見、対立しているように見えるが、矛盾していない

    • 関数型プログラミングで、可動部を極力減らす
    • 残った可動部を、オブジェクト指向プログラミングでカプセル化する