2022年5月7日 • ☕️ 3 min read

関数型プログラミングと言えば、再帰処理を思い出すでしょう。再帰処理には末尾再帰の有無がスタックオーバーフロー、いわゆるメモリの過消費を防ぐために必要です。末尾再帰について、こちらのwikiをご覧ください。JavaScriptのes6から、末尾再帰を実装できるようになりました。ただし、JavaScriptの世界にわずかのところにしかサポートされていません。V8エンジンがサポートしていないため、nodejsはまずアウトです。ブラウザ側は、後進者のSafari以外サポートしていません。

今のTCO(末尾再帰)のサポート情報: https://kangax.github.io/compat-table/es6/#test-proper_tail_calls_(tail_call_optimisation)

末尾再帰の発動条件

Wikipediaに書いている通り、最後のステップに再帰functionをそのままreturnすることで、再帰以外の計算がなかったら末尾再帰と判断されます。

WebKitの仕様はこちら:ECMAScript 6 Proper Tail Calls in WebKit

よく使われる階乗関数を例として。

TCOが有効ではない
Copy
function iterfact(n) {
  if (n === 1n) {
    return 1n;
  }
  
  // 計算(n*)が入っているので、末尾再帰ではない
  return n * iterfact(n - 1n);}

計算を消すために、今まで計算した結果を保持しておくパラメータが必要となります。末尾再帰が有効になっている例はこちらです。

TCOが有効
Copy
function iterfact(n, acc = 1n) {
  if (n === 1n) {
    return acc;
  }

  // iterfactをそのままreturnしているので、末尾再帰と判断される
  return iterfact(n - 1n, n * acc);}

末尾再帰をSafariに有効にする方法

上のステップに書いてある再帰functionを計算をかけずにreturnすること以外、strict modeが有効になっていることも必要です。

以下のようになります。

Copy
'use strict';
function iterfact(n, acc = 1n) {
.
.
.

サンプル

下記のCodeSandboxをChromeとSafariで実行してみてください。

Chromeの場合。 tco_chrome.png

Safariなら問題ありません。 tco_safari.png

回避策

エレガントではないですが、再帰関数を使わない方法はもちろんあります。階乗関数を再帰なしでも簡単にかけるでしょう。

Copy
function loopfact(n) {
  let result = 1n;
  while (n > 1n) {
    result *= n;
    n--;
  }
  return result;
}

そうすると、末尾再帰を最適化できないブラウザでもエラーなし動くでしょう。ただし、木構造などのデータを処理するとき、やっぱり再帰の方がわかりやすく思います。

ではでは。


ThunderMiracle

Blog part of ThunderMiracle.com