ES2015 では 末尾再帰に関して仕様に書かれています。関数が末尾再帰している場合に最適化をすることになっています。
そもそも末尾再帰とは何かを Wikipedia で調べると以下の用に書いてあります。
末尾再帰(まつびさいき)とは、再帰的な関数やプロシージャにおいて、自身の再帰呼び出しが、その計算における最後のステップになっているような再帰のパターンのことである。
例えばこんな感じの関数のことです。
function factorial(a, b = 1) {
if (a === 1) return b;
return factorial(a - 1, a * b);
}
これは階乗を計算する関数です。関数の最期で自分自身を呼び出した結果を返しています。これが末尾再帰です。
更にこの末尾再帰を for
や while
構文を使った形に変換することを末尾再帰最適化といいます。関数呼び出しはコストの高い処理なので変換できればメモリなどのリソースの節約になりますし、 Max Call Stack
などのエラーを防ぐこともできます。
Babel も仕様にあわせて実装してあるみたいなので試してみます。
変換
上に貼ったコードがそのまま末尾再帰の例なので Babel で変換してみます。 Babel 5.8.23 で変換した結果が以下です
function factorial(_x2) {
var _arguments = arguments;
var _again = true;
_function: while (_again) {
var a = _x2;
b = undefined;
_again = false;
var b = _arguments.length <= 1 || _arguments[1] === undefined ? 1 : _arguments[1];
if (a === 1) return b;
_arguments = [_x2 = a - 1, a * b];
_again = true;
continue _function;
}
}
自分自身を呼び出すす return
が消えて while
と continue
を使う形になっているのがわかると思います。
変換されないパターン
この factroial
関数の実装なんですが、ナイーブな実装だと以下のようになると思います。
function factorial_2(a) {
if (a === 1) return 1;
return a * factorial_2(a - 1);
}
これの変換結果が以下です。
function factorial_2(a) {
if (a === 1) return 1;
return a * factorial_2(a - 1);
}
そのままです。 return
に渡すのが自身の呼び出し結果出ないと最適化してくれないようです。そのため上の例では引数を2つ受け取る形になっています。
function sum(a, ...x) {
if (a == null) return 0;
return a + sum(...x);
}
これは ...
演算子を使った引数の合計を返す関数です。再帰をつかっていますが変換してくれません。この場合は以下のようにすれば変換されました。こういうのを考えるのはパズル的で面白い気がします。
function sum_2(x1, x2, ...xs) {
if (x2 == null) return x1;
return sum_2(x1 + x2, ...xs);
}
再帰を使ったほうが簡潔にわかりやすくかけることもあるので使っていきたいです。しかし、最適化されなかった場合にコールスタックがあふれたりするのもこわいので、おとなしくループなり forEach
map
filter
reduce
なりを使ったほうがいいのかもしれません。