7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

『Don’t Be Scared Of Functional Programming』 PHPで関数型プログラミングに触れてみる。

Last updated at Posted at 2015-01-16

はじめに

これは『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の処理も汎用処理ですので、関数化しちゃいます。

a足すb
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, $averageTempsarrayにまとめる

現在$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と比べると…うーん。
可読性だけで言えば、常に関数型プログラミングが高いかと言われるとそうでもない感じ。
慣れもあるかな?
でも処理が多くなればなるほど、関数型の方が可読性が上がりそう。

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?