スタックについてはあまり心配しないでください。スタック フレームを使用して関数呼び出しを実装する必要があるという基本的なことは何もありません。これは、それらを実装するための 1 つの可能な手法にすぎません。
「スタック」を持っている場合でも、スタックを使用可能なメモリのごく一部に制限する必要があると言うものは何もありません。これは基本的に、命令型プログラミングに合わせて調整されたヒューリスティックです。問題解決手法として再帰を使用しない場合、無限再帰のバグが原因でコール スタックが非常に深くなる傾向があり、スタック サイズを非常に小さいものに制限すると、そのようなプログラムは、使用可能なすべてのメモリとスワップを消費する代わりに、すぐに終了します。死んでいる。
関数型プログラマーにとって、コンピューターがまだギガバイトの RAM を使用できる場合に、より多くの関数呼び出しを行うためにメモリを「使い果たし」、プログラムを終了させることは、言語設計のばかげた欠陥です。これは、ループを任意の反復回数に制限する C のようなものです。したがって、関数型言語がスタックを使用して関数呼び出しを実装している場合でも、可能であれば C で知られている標準の小さなスタックの使用を避けたいという強い動機があります。
実際、Haskell にはオーバーフローする可能性のあるスタックがありますが、それは C でおなじみのコール スタックではありません。呼び出し深度の制限。Haskell が持っているスタックは、決定を下すためにもう少し評価する必要がある「保留中」の値を追跡するために使用されます (これについては後で詳しく説明します)。この種のスタック オーバーフローの詳細については、こちらを参照してください。
例を見て、コードがどのように評価されるかを見てみましょう。1ただし、あなたの例よりもさらに単純な例を使用します。
main = do
input <- getLine
if input == "quit"
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ input
main
Haskell の評価は遅延2です。簡単に言えば、それは、決定を下すためにその項の値が必要な場合にのみ項を評価することを意味します。たとえば、計算してその結果をリストの先頭に追加すると、リスト31 + 1
に「保留中」として残すことができます。しかし、結果が 3 に等しいかどうかをテストするために使用すると、 Haskell は に変換する作業を実際に行う必要があります。1 + 1
if
1 + 1
2
しかし、それだけでは何も起こりません。プログラム全体が「保留中」の値として残されます。main
しかし、それを実行するために、IO アクションが何に評価されるかを知る必要がある外部ドライバーがあります。
例に戻ります。そのブロックmain
と同じです。do
の場合IO
、do
ブロックは一連の小さなアクションから大きなIO
アクションを作成し、順番に実行する必要があります。そのため、Haskell ランタイムは、まだ必要ではない未評価のものが続くとmain
評価します。input <- getLine
キーボードから読み取り、結果を呼び出す方法を知っていれば十分String
input
です。「foo」と入力したとします。これにより、Haskell は「次の」IO
アクションとして次のようなものを残します。
if "foo" == "quit"
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ "foo"
main
Haskell は最も外側のものだけを見ているので、これはほとんど「もし何とか何とか何とか...」のように見えます。if
IO-executor が何かを実行できるものではないため、何を返すかを確認するために評価する必要があります。if
どちらかthen
のelse
ブランチに評価されるだけですが、条件を評価するには Haskell がどちらの決定を下す必要があるかを知る必要があります。したがって、次のようになります。
if False
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ "foo"
main
これにより、全体if
を次のように縮小できます。
do
putStrLn $ "You typed: " ++ "foo"
main
そして再び、順序付けられた一連のサブアクションからなるアクションがdo
得られます。IO
したがって、IO-executor が次にしなければならないことは、putStrLn $ "You typed: " ++ "foo"
. しかし、それもIO
アクションではありません (結果が 1 になるはずの未評価の計算です)。ですから、それを評価する必要があります。
の「最も外側」の部分は、putStrLn $ "You typed: " ++ "foo"
実際には$
です。Haskell ランタイムと同じように表示できるように、中置演算子の構文を削除すると、次のようになります。
($) putStrLn ((++) "You typed: " "foo")
しかし、$
によって定義されたばかりの演算子な($) f x = f x
ので、右辺を代入するとすぐに次のようになります。
putStrLn ((++) "You typed: " "foo")`
通常、これは の定義に代入して評価しますがputStrLn
、これは Haskell コードで直接表現できない「魔法の」プリミティブ関数です。したがって、実際にはこのように評価されません。外側の IO-executor は、それをどう処理するかを単純に知っています。ただし、 の引数を完全に評価する必要があるputStrLn
ため、そのままにしておくことはできません(++) "You typed: " "foo"
。
実際には、その式を完全に評価するには++
、リスト操作の観点から の定義を行う多くのステップがありますが、それを飛ばして、 に評価されるとしましょう"You typed: foo"
。そのため、IO-executor は (テキストをコンソールに書き込む) を実行し、ブロックputStrLn
の 2 番目の部分に進むことができます。do
`main`
これは、アクションとしてすぐに実行できるものではありませんIO
(Haskell のように組み込まれていませputStrLn
んgetLine
) main
。
do
input <- getLine
if input == "quit"
then
putStrLn "Good-bye!"
else do
putStrLn $ "You typed: " ++ input
main
そして、残りがどこに向かっているのかを見ることができると確信しています。
どの種類のスタックについても何も述べていないことに注意してください。IO
これらはすべて、アクションを記述するデータ構造を構築しているだけなmain
ので、外部ドライバーはそれを実行できます。特に特別なデータ構造ではありません。評価システムの観点からは、他のデータ構造とまったく同じであり、そのサイズに恣意的な制限はありません。
この場合、遅延評価は、このデータ構造の生成がその消費とインターリーブされることを意味します (そして、その後の部分の生成は、その前の部分を消費した結果として何が起こったかに依存する可能性があります!)、したがって、このプログラムは実行できます一定量のスペース。しかし、質問に対するshachafのコメントで指摘されているように、これは不要なスタックフレームを削除するための最適化ではありません。それは、遅延評価で自動的に起こることです。
それで、何が起こっているのかを理解するのに十分に役立つことを願っています. 基本的に、Haskell が への再帰呼び出しを評価するまでgetCommandsFromUser
には、前の繰り返しで生成されたすべてのデータが既に処理されているため、ガベージ コレクションが行われます。したがって、一定量以上のメモリを必要とせずに、このプログラムを無期限に実行し続けることができます。これは単に遅延評価の単純な結果であり、IO
が関係していても実質的に違いはありません。
1現在の Haskell の実際の実装については詳しく知らないことを前もってお断りしておきます。しかし、私はHaskellのような怠惰な純粋言語を実装するための一般的なテクニックを知っています. また、詳細に飛び込みすぎないようにして、物事がどのように機能するかを直感的に説明するだけにします。したがって、この説明は、コンピューター内で実際に何が起こっているかについての詳細の一部が間違っている可能性がありますが、これらのことがどのように機能するかを示しているはずです.
2言語仕様では、技術的には、評価は「非厳密」であるべきだと言っているだけです。これから説明する評価は、非公式に「怠惰な」評価として知られていますが、実際には可能な「非厳密な」評価戦略の 1 つにすぎませんが、実際に得られるものです。
3(1 + 1) : originalList
そして、新しいリストは実際には、それが空かどうかを誰かが知る必要があるまで、「保留中」の結果として残すことができます。