私はSICPの本を使ってLISPを学び始めています。著者は、手順(つまり関数)は再帰的または反復的である可能性があると述べています。さらに、これらのプロシージャが生成するプロセスも再帰的または反復的であり、驚くべきことに、再帰的プロシージャが反復プロセスを生成する場合があります。
与えられた例は階乗手順です。これは再帰的手順ですが、反復プロセスを生成します。
(define factorial n)
(iter 1 1 n))
(define (iter product counter max-count)
(if (> counter max-count)
product
(iter (* counter product)
(+ counter 1)
max-count)))
そして、これが本からの引用です:
プロセスとプロシージャの違いがわかりにくい理由の1つは、一般的な言語(Ada、Pascal、Cを含む)のほとんどの実装が、再帰プロシージャの解釈がメモリの量を消費するように設計されていることです。説明されているプロセスが原則として反復的な場合でも、プロシージャコールの数検討するSchemeの実装は、この欠点を共有していません。反復プロセスが再帰的プロシージャによって記述されている場合でも、一定のスペースで反復プロセスを実行します。
質問:関連する原則(つまり何)は理解しましたが、LISPインタープリター/コンパイラーが再帰関数から反復プロセスを生成する方法を理解していません。定数スペースを使用して計算できます。 、そして他のほとんどの言語がそれを行うことができない理由。
基本的に末尾再帰では、一般的な再帰の問題は、各関数呼び出しが独自のスタックフレームを取得することです。このスタックフレームには、すべてのローカル変数とその他のものが格納されます。
したがって、次のような関数を実行すると
int fact(int n){
if(n == 0)
return 1;
return n * fact(n-1);
}
fact
はn
回呼び出され、n
スタックフレームを割り当てます。 n
が大きい場合、それはメモリのlotです。
しかし、私たちの機能が次の形式で構造化されている場合、希望があります
int f(int x){
...
return g(foo); // Function call is in the final position
}
次に、f
に入る前にg
のスタックフレームを破棄できます。これは、単純な末尾呼び出しを使用している限り、あまり多くのフレームを割り当てないことを意味します。実際には、独自のラベルを持ち、functionと呼ばれる末尾にjmp
ingする各関数に非常に高速にコンパイルされます。
SML、OCaml、Haskell、Scala、およびClojure(の一種)のようなほとんどの関数型言語の実装と同様に、すべてのスキーマは末尾再帰です。これは、関数の最後に関数呼び出しがある場合は常に、新しいスタックフレームを割り当てないことを意味します。これを使用して、Schemeで次のようにdo-whileループを記述できます。
(define (do-while pred body)
(body) ;; Execute the body
(if (pred) ;; Check the predicate
(do-while pred body) ;; Tail call
'())) ;; Exit
そして、これは同等の命令型コードとまったく同じ量のスペースで実行されます:)かなり気の利いた。
よくある誤解は、TCOは関数の末尾呼び出しの場合に厳密に限定されているというものです。これはTCOの特定のサブセットであり、ほとんどのJVM言語が提供するものです。 JVMの制限によって制限されていないSchemeなどの言語は、適切にTCOされているため、状態間で末尾呼び出しを行うことにより、定数メモリで実行されるDFAを作成できます。