Laravel/Collection 勉強会資料 関数型プログラミングについて語る
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
手続き型
for文使う例
<?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; }) );
- セミコロン
;
ではなく、カンマ,
区切りで処理を記述していくことから、
"programmable comma"と呼ばれたりする
各手法の特徴
手続き型 | 関数型 (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
の嬉しさがわかったところで... -> 使い方のお勉強
- よく使いそうなやつ
- わかりやすいやつ
おまけ
OO makes code understandable by encapsulating moving parts.
FP makes code understandable by minimizing moving parts.オブジェクト指向は可動部をカプセル化することでコードを理解しやすくする。
関数型プログラミングは可動部を極力減らすことでコードを理解しやすくする。
- 「可動部」 = 状態(state)のことでしょう
- 局所変数
- 一時変数
- 関数の引数
- メンバ変数
- 局所変数
- 一見、対立しているように見えるが、矛盾していない
- 関数型プログラミングで、可動部を極力減らす
- 残った可動部を、オブジェクト指向プログラミングでカプセル化する