はじめに
これは『Don’t Be Scared Of Functional Programming』 JavaScriptで関数型言語に触れてみる。
を拝見して、分かりやすくて面白そうだったのでPHP版をやってみたものです。
kaznmbさんの記事同様、Don't Be Scared of Functional Programmingを参考にしておりますので、
そちらも併せてご覧下さい。(リンク先で使用されているのはJavaScriptです)
コードなどもお二人の記事を参考にしつつ、PHP用に書き換えていく手法でやってみます。
関数型プログラミングの基本コンセプト
以前の記事に記述しましたので、詳細は割愛しますが、
- 副作用がないこと。
- 参照透過性を大切にすること。
kaznmbさんとDon't Be Scared of Functional Programmingでは、
- Immutable
- Stateless
として表現されていますが、ほぼ同じ意味だと思っています。
関数型プログラミングらしい実装のために
kaznmbさんの記事に
慣れていないと、どうしても関数型プログラミングっぽくない実装をしてしまいます。
よりベストプラクティスに沿うように、以下のルールを守りましょう。
とあるように、私も同じルールを引き継ぎ、実装してみようと思います。
- 【ルール1】全ての関数は、最低でも1つの引き数を取ります。
- 【ルール2】全ての関数が、何かしらの値、もしくは関数を返します。
- 【ルール3】ループは使用禁止。
実際にやってみよう
こんな値を返すAPIがあったとします。
$data = array(
array(
'name' => 'Jamestown',
'population' => '2047',
'temperatures' => array(-34, 67, 101, 87),
),
array(
'name' => 'Awesome Town',
'population' => '3568',
'temperatures' => array(-3, 4, 9, 12),
),
array(
'name' => 'Funky Town',
'population' => '1000000',
'temperatures' => array(75, 75, 75, 75),
),
);
人口(population)と平均気温(temperature)をグラフに可視化したいとします。
グラフを描画するために、まずは上記のデータを以下の様な状態にしたいです。
xが平均気温、yが人口を表しています。
array(
array(x, y),
array(x, y),
...
);
関数型プログラミングを意識せず実装してみると以下のような感じ。
$result = array();
foreach ($data as $detail) {
$result[] = array(
array_sum($detail['temperatures']) / count($detail['temperatures']),
$detail['population'],
);
}
ほら、従来だとこんなに読みにく…あれ?読みやすい!?
これだけ短くて分かりやすければこれで良いんじゃ…?
しかし勉強のため、関数型プログラミングの手法を試してみます。
必要な論理を考える
// $data から key 'population' の value を取得して配列にする
$populations =
// $data から key 'temperatures' の value を取得して配列にする
$allTemperatures =
// $allTemperatures からそれぞれの平均を算出する
$averageTemps =
// $populations, $averageTemps を array にまとめる
$combineArrays =
ではまず上からコードにしてみます。
1. 配列から指定したキーの値を取得して配列にする
-
$data
からkey
'population'
のvalue
を取得して配列にする -
$data
からkey
'temperatures'
のvalue
を取得して配列にする
この2つは、
- 配列
array
から指定したキーspecified
の値value
を取得して配列にする
機能と言えますので、まずは
- 配列 から 指定したキー の 値 を取得する
という部分を一つの関数として実装します。
function getSpecifiedKey($specified)
{
return function ($value) use ($specified) {
return $value[$specified];
}
}
私はぱっと見$value
がなぜ突然出てくるのか分からなくて混乱しましたが、
関数そのものを展開せず返しているだけと思えば分かりやすかったです。
(同様の質問とその回答本家記事のコメントにもありました)
次に「配列にする」ところまでを関数化します。
function pluck($array, $specified)
{
return array_map(getSpecifiedKey($specified), $array);
}
これらをまとめると以下の様になります。
function pluck($array, $specified)
{
return array_map(getSpecifiedKey($specified), $array);
}
function getSpecifiedKey($specified)
{
return function ($value) use ($specified) {
return $value[$specified];
}
}
...
$populations = pluck($data, 'population');
$allTemperatures = pluck($data, 'temperatures');
2. 配列の各値(配列)それぞれの平均を算出する
-
$allTemperatures
からそれぞれの平均を算出する
現在$allTemperatures
は
// allTemperatures
array(array(-34, 67, 101, 87), array(-3, 4, 9, 12), array(75, 75, 75, 75));
上記のような二次元配列構造になっているので、
各値(配列)に使用する、平均を算出する関数を作成します。
平均を算出するために必要なものは、 値の平均=「値の合計」÷「値の個数」 なので、
それぞれを求める処理を関数化します。
まずは「値の合計」を算出する関数を作成します。
とりあえず「ループを使わない」約束に則り、再帰で関数化します。
function sumForArray($array, $carry = 0)
{
$carry += $array[0];
$item = array_slice($array, 1);
if (count($item) == 0) {
return $carry;
}
return sumForArray($item, $carry);
}
良い感じ。
しかし知っての通り、PHPにはarray_reduce
という便利関数が存在しているので、
そちらを使用して実装しなおします。
function sumForArray($array)
{
return array_reduce($array, function ($carry, $item) {
return $carry + $item;
}, 0);
}
array_sum
あるよねと思われた方も多いと思いますが、勉強のためとお思い下さい。
$carry + $item
の処理も汎用処理ですので、関数化しちゃいます。
function plus($a, $b)
{
return $a + $b;
}
「値の個数」に関しては、PHPにはcount
がありますので、そちらを使います。
計算できる材料が揃いましたので、まずは平均を算出する関数を作成します。
function average($total, $count)
{
return $total / $count;
}
これだけでは配列の平均を出すには少し役不足のため、array_average
関数を作ります。
function array_average($array)
{
return average(sumForArray($array), count($array));
}
これらをまとめると以下の様になります。
function array_average($array)
{
return average(sumForArray($array), count($array));
}
function average($total, $count)
{
return $total / $count;
}
function sumForArray($array)
{
return array_reduce($array, function ($carry, $item) {
return plus($carry, $item);
}, 0);
}
function plus($a, $b)
{
return $a + $b;
}
...
$averageTemps = array_map(function ($value) {
return array_average($value)
}, $allTemperatures);
3. 2つの配列を1つにまとめる
-
$populations
,$averageTemps
をarray
にまとめる
現在$populations
と$averageTemps
は
// populations
array(2047, 3568, 1000000);
// averageTemps
array(55.25, 5.5, 75);
上のような形になっているので、
array(
array(55.25, 2047),
array(5.5, 3568),
array(75, 1000000),
);
こんな形に整形する関数を実装します。
これもルールに従い、ループを使わず再起で実装します。
function combineArrays($array1, $array2, $result = array())
{
$result[] = array($array1[0], $array2[0]);
$remainingArray1 = array_slice($array1, 1);
$remainingArray2 = array_slice($array2, 1);
if (count($remainingArray1) > 0 && count($remainingArray2) > 0) {
return combineArrays($remainingArray1, $remainingArray2, $result);
}
return $result;
}
...
$processed = combineArrays($populations, $averageTemps);
まとめる
sumForArray
はもうarray_sum
に書き換えてしまいます。
そうなるとplus
関数も必要なくなるので、その辺りも調整して、できあがりです。
/**
* 2つの配列を1つにまとめる
*/
function combineArrays($array1, $array2, $result = array())
{
$result[] = array($array1[0], $array2[0]);
$remainingArray1 = array_slice($array1, 1);
$remainingArray2 = array_slice($array2, 1);
if (count($remainingArray1) > 0 && count($remainingArray2) > 0) {
return combineArrays($remainingArray1, $remainingArray2, $result);
}
return $result;
}
/**
* 配列の平均を算出する
*/
function array_average($array)
{
return average(array_sum($array), count($array));
}
/**
* 平均を算出する
*/
function average($total, $count)
{
return $total / $count;
}
/**
* 配列の指定したキーの値を返して配列にする
*/
function pluck($array, $specified)
{
return array_map(getSpecifiedKey($specified), $array);
}
/**
* 指定したキーの値を返す
*/
function getSpecifiedKey($specified)
{
return function ($value) use ($specified) {
return $value[$specified];
};
}
$populations = pluck($data, 'population');
$allTemperatures = pluck($data, 'temperatures');
$averageTemps = array_map(function ($value) {
return array_average($value);
}, $allTemperatures);
$processed = combineArrays($averageTemps, $populations);
やってみて
思ったより処理が長くなってしまった印象。最初のforeach
と比べると…うーん。
可読性だけで言えば、常に関数型プログラミングが高いかと言われるとそうでもない感じ。
慣れもあるかな?
でも処理が多くなればなるほど、関数型の方が可読性が上がりそう。