原則として、テール コールは常に呼び出し関数のスタック フレームを再利用できます。ただし、一部のランタイム環境 (Java VM など) には、末尾の呼び出しでスタック フレームを効率的に再利用するためのプリミティブがありません。したがって、生産品質の Scala 実装は、最後のアクションがそれ自体への呼び出しである直接末尾再帰関数のスタック フレームを再利用するためにのみ必要です。他の末尾呼び出しも最適化される可能性がありますが、実装間でこれに依存するべきではありません。
この段落の真ん中の 2 つの文の意味を説明できる人はいますか?
末尾再帰は、末尾呼び出しの特殊なケースです。直接末尾再帰は末尾再帰の特殊なケースです。直接末尾再帰のみが最適化されることが保証されています。他のものも最適化される可能性がありますが、それは基本的にコンパイラの最適化にすぎません。言語機能として、Scala は直接末尾再帰の除去のみを保証します。
それで、違いは何ですか?
末尾呼び出しは、サブルーチンの最後の呼び出しです。
def a = {
b
c
}
この場合、への呼び出しc
は末尾呼び出しであり、への呼び出しb
はそうではありません。
末尾再帰とは、末尾呼び出しが以前に呼び出されたサブルーチンを呼び出す場合です。
def a = {
b
}
def b = {
a
}
これが末尾再帰です:a
呼び出しb
(末尾呼び出し) で、これがa
再び呼び出します。(以下で説明する直接末尾再帰とは対照的に、これは間接末尾再帰と呼ばれることもあります。)
ただし、2 つの例はいずれも Scala によって最適化されません。または、より正確には、Scala 実装はそれらを最適化できますが、そうする必要はありません。O(1)
これは、上記のすべてのケースでスタック スペースが必要になることが言語仕様で保証されているたとえば、Scheme とは対照的です。
Scala 言語仕様は、直接末尾再帰が最適化されることのみを保証します。つまり、サブルーチンが他の呼び出しを間に挟まずにそれ自体を直接呼び出す場合です。
def a = {
b
a
}
この場合、への呼び出しa
は末尾呼び出し (サブルーチンの最後の呼び出しであるため) であり、末尾再帰 (自分自身を再度呼び出すため) であり、最も重要なのは直接末尾再帰です。a
最初に別の呼び出し。
メソッドが直接末尾再帰的でなくなる原因となる微妙なことがたくさんあることに注意してください。たとえば、a
がオーバーロードされている場合、再帰は実際にはさまざまなオーバーロードを通過する可能性があるため、直接的ではなくなります。
実際には、これは次の 2 つのことを意味します。
- 少なくとも末尾呼び出しを含めずに、末尾再帰メソッドで Extract Method Refactoring を実行することはできません。最適化されています)。
- 直接末尾再帰のみを使用できます。間接的な末尾再帰を使用して非常にエレガントに表現できる末尾再帰降下パーサー、またはステート マシンが出てきました。
これの主な理由は、基礎となる実行エンジンにGOTO
、継続、ファーストクラスの可変スタック、または適切な末尾呼び出しなどの強力な制御フロー操作機能がない場合、その上に独自のスタックを実装するか、トランポリンを使用する必要があるためです。一般化された適切な末尾呼び出しを提供するために、グローバル CPS 変換または同様の厄介なものを作成します。これらはすべて、パフォーマンスに重大な影響を与えるか、同じプラットフォーム上の他のコードとの相互運用性をもたらします。
または、Clojure の作成者である Rich Hickey が同じ問題に直面したときに言ったように、「パフォーマンス、Java 相互運用性、テール コール。2 つ選んでください」。Clojure と Scala はどちらも末尾呼び出しで妥協し、完全な末尾呼び出しではなく末尾再帰のみを提供することを選択しました。
簡単に言うと、あなたが投稿した特定の例は直接末尾再帰であるため、最適化されます。@tailrec
メソッドに注釈を付けることで、これをテストできます。メソッドが最適化されるかどうかにかかわらず、注釈は変更されませんが、メソッドが最適化されない場合はコンパイル エラーが発生することが保証されます。
上記の微妙な点の@tailrec
ため、コンパイル エラーを取得するためだけでなく、コードを保守している他の開発者へのヒントとしても、最適化が必要なメソッドに注釈を付けることをお勧めします。