PHP

PHP: 関数合成する関数を作る方法【愚直にforeach編】

前回はPHP: 関数合成する関数を作る方法 - Qiitaという投稿をした。そこで紹介した関数合成する関数の実装は次のコードだった。

$composeAll = function (callable $function, callable ...$functions): callable {

return array_reduce(
$functions,
function (callable $f, callable $g): callable {
return function (...$arguments) use ($f, $g) {
return $g($f(...$arguments));
};
},
$function
);
};

この関数は、関数の配列をreduceしていってひとつの関数を作る設計になっているが、ものすごく大量の関数を合成することは想定されていない。

試してみると分かるが、2つの関数を合成するたびにメモリが増えていき、memory_limitに達するとFatal Errorが発生する。

$increment = function (int $value): int {

return $value + 1;
};

// 1,048,576個の関数を合成する
$increments1048576 = array_fill(0, 1024 * 1024, $increment);

$add1048576 = $composeAll(...$increments1048576); // Fatal Error

実行結果:

Terminated due to signal: SEGMENTATION FAULT (11)

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) in /.../Untitled 3.php on line 11

これは、関数合成1回ごとにつなぎの関数のためのメモリが確保されていくから仕方がないところ。イメージとしては、こんなかんじで変数を大量に定義するのと同じ:

$increments1_2 = function (...) { return $increment2($increment1(...)); };

$increments1_2_3 = function (...) { return $increment3($increments1_2(...)); };
$increments1_2_3_4 = function (...) { return $increment4($increments1_2_3(...)); };
$increments1_2_3_4_5 = function (...) { return $increment5($increments1_2_3_4(...)); };
$increments1_2_3_4_5_6 = function (...) { return $increment6($increments1_2_3_4_5(...)); };
...1048570個くらい略...
$increments1_2_3_4_5_6_snip_1048576 = function (...) { return $increment1048576($increments1_2_3_4_5_snip_1048575(...)); };

100万個もの関数を合成することはそうそうないので、実際は直面する可能性が低い軽微な問題ではある。

それでも性能上の限界があることは記憶に留めておきたい。そして、限界はメモリだったりと実行時の状況次第で限界点のしきい値が変わるということも。

プログラムの完成度として、できるだけ限界がないようにしておきたいときは、愚直にforeachする方法もある:

$increment = function (int $value): int {

return $value + 1;
};

$composeAll = function (callable ...$functions): callable {
return function ($argument) use ($functions) {
foreach ($functions as $function) {
$argument = $function($argument);
}
return $argument;
};
};

$increments1048576 = array_fill(0, 1024 * 1024, $increment);

$add1048576 = $composeAll(...$increments1048576);

echo $add1048576(0); //=> 1048576