勉強日記

チラ裏

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

weeyble-php.connpass.com

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; })
);

各手法の特徴

手続き型 関数型
(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.

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