Haskellがデフォルトで怠惰であることは誰もが知っています(または知っておくべきです)。評価する必要があるまで、何も評価されません。
いいえ。
Haskellは怠惰な言語ではありません
Haskellは、副作用がないため、評価の順序が重要ではない言語です。
言語は無限ループを許容するため、評価の順序が重要ではないというのはまったく真実ではありません。注意しないと、評価順序が異なると有限時間で終了する場合に、部分式を永久に評価する袋小路に陥る可能性があります。したがって、次のように言う方が正確です。
- Haskellの実装は、終了する評価順序がある場合に終了する方法でプログラムを評価する必要があります。考えられるすべての評価オーダーが終了しない場合にのみ、実装を終了できません。
これでも、実装はプログラムを評価する方法に大きな自由があります。
Haskellプログラムは単一の式、つまりlet {
すべてのトップレベルのバインディング} in Main.main
です。評価は、式(実行中のプログラムの現在の状態を表す)を変更する一連の縮小(小さな)ステップとして理解できます。
削減ステップは、2つのカテゴリに分けることができます。必要であることが証明されているもの(削減の終了シーケンスの一部であることが証明されているもの)とそうでないものです。証明可能に必要な削減は、「明らかに」必要なものと、必要であることを証明するために重要な分析を必要とするものの2つのサブカテゴリにやや漠然と分割できます。
明らかに必要な削減のみを実行することは、いわゆる「遅延評価」です。Haskellの純粋に遅延評価の実装がこれまでに存在したかどうかはわかりません。抱擁は1つだったかもしれません。GHCは間違いなくそうではありません。
GHCは、コンパイル時に、おそらく必要ではない削減手順を実行します。たとえば、結果が使用されることを証明できない場合でも、に置き換え1+2::Int
られます。3::Int
GHCは、状況によっては、実行時に必要ではない削減を実行する場合もあります。たとえば、を評価するコードを生成するときに、とがf (x+y)
型でx
あり、それらの値が実行時に既知であるが、その引数を使用することが証明できない場合、を呼び出す前に計算しない理由はありません。使用するヒープスペースとコードスペースが少なく、引数が使用されていない場合でもおそらく高速です。しかし、GHCが実際にこのような最適化の機会を利用しているかどうかはわかりません。y
Int
f
x+y
f
GHCは、実行時に確実に評価ステップを実行します。これは、かなり複雑なクロスモジュール分析によってのみ必要であることが証明されています。これは非常に一般的であり、現実的なプログラムの評価の大部分を表す可能性があります。遅延評価は、最後の手段であるフォールバック評価戦略です。それは原則として起こることではありません。
実行時にはるかに投機的な評価を行ったGHCの「楽観的評価」ブランチがありました。パフォーマンスが良くなかったためではなく、複雑さと継続的なメンテナンスの負担のために放棄されました。HaskellがPythonやC++と同じくらい人気があったとしたら、資金力のある企業によって維持されている、はるかに洗練されたランタイム評価戦略を備えた実装があると確信しています。非遅延評価は言語の変更ではなく、単なるエンジニアリング上の課題です。
削減はトップレベルのI/Oによって推進され、他には何もありません
次のような特別な副作用のある削減ルールを使用して、外界との相互作用をモデル化できます。「現在のプログラムがの形式の場合、getChar >>= <expr>
標準入力から文字を取得し、取得した文字に<expr>
適用されるようにプログラムを削減します。」
ランタイムシステムの全体的な目標は、これらの副作用の形式の1つになるまでプログラムを評価し、次に副作用を実行してから、プログラムがなどの終了を意味する形式になるまで繰り返すことreturn ()
です。
何がいつ削減されるかについては、他にルールはありません。何が何に還元できるかについてのルールだけがあります。
たとえば、if
式の唯一のルールは、if True then <expr1> else <expr2>
に減らすことができ、<expr1>
に減らすことができ、は例外的な値である場合は、例外的な値に減らすことができます。if False then <expr1> else <expr2>
<expr2>
if <exc> then <expr1> else <expr2>
<exc>
プログラムの現在の状態を表す式が式である場合、それが式またはにif
達するまで条件の削減を実行する以外に選択肢はありません。これは、式を削除して到達する希望がある唯一の方法だからです。I/Oルールの1つに一致する状態。しかし、言語仕様では、それほど多くの言葉でそれを行うように指示されていません。True
False
<exc>
if
この種の暗黙的な順序付けの制約は、評価を「強制的に」実行できる唯一の方法です。これは、初心者にとって頻繁な混乱の原因です。たとえば、人々は時々、の代わりにfoldl
書くことによってより厳密にしようとします。これは機能しません。また、式自体を評価させることはできないため、これまでにないような機能があります。評価は「上から来る」ことしかできません。この点で特別なことではありません。foldl (\x y -> x `seq` x+y)
foldl (+)
seq
削減はどこでも起こります
Haskellの削減(または評価)は、厳密なポイントでのみ発生します。[...]私の直感によると、main、seq / bangパターン、パターンマッチング、およびmainを介して実行されるIOアクションは、主要な厳密点です[...]。
その声明を理解する方法がわかりません。プログラムのすべての部分には何らかの意味があり、その意味は削減ルールによって定義されるため、削減はどこでも行われます。
関数適用を減らすには、ルールに一致するなどの形式になるまで<expr1> <expr2>
評価する必要があります。しかし、何らかの理由で、関数適用は「評価を強制する」とされる式のリストに表示されない傾向がありますが、常に表示されます。<expr1>
(\x -> <expr1'>)
(getChar >>=)
case
この誤解は、別の回答にあるHaskellwikiからの引用で見ることができます。
実際には、Haskellは純粋に怠惰な言語ではありません。たとえば、パターンマッチングは通常厳密です。
ランタイムが何もしないためにすべてのプログラムがハングする言語を除いて、それを書いた人にとって「純粋に怠惰な言語」と見なされるものが何であるかはわかりません。パターンマッチングがあなたの言語の特徴であるなら、あなたはある時点で実際にそれをしなければなりません。それを行うには、それがパターンに一致するかどうかを判断するために十分に精査者を評価する必要があります。これは、原則として可能なパターンに一致させるための最も怠惰な方法です。
~
-接頭辞付きのパターンは、プログラマーによって「怠惰」と呼ばれることがよくありますが、言語仕様では「反駁できない」と呼ばれています。それらの定義特性は、それらが常に一致することです。それらは常に一致するため、それらが一致するかどうかを判断するために精査者を評価する必要はありません。したがって、遅延実装は一致しません。通常のパターンと反駁できないパターンの違いは、使用することになっている評価戦略ではなく、それらが一致する式です。仕様は評価戦略については何も述べていません。
main
厳しさのポイントです。これは、そのコンテキストの主要な厳密点であるプログラムとして特別に指定されています。プログラム(main
のコンテキスト)が評価されると、mainの厳密点がアクティブになります。[...] Mainは通常、IOアクションで構成されます。これは、コンテキストが。である厳密性ポイントでもありますmain
。
そのどれにも意味があるとは思いません。
メインの深さは最大です:それは完全に評価されなければなりません。
いいえ、main
I / Oアクションをトップレベルに表示するには、「浅く」評価する必要があります。main
はプログラム全体であり、すべてのコードがすべての実行に関連しているわけではないため、プログラムはすべての実行で完全に評価されるわけではありません(一般的に)。
これらの用語で話し合いseq
、パターンマッチングを行います。
パターンマッチングについてはすでに話しました。seq
アプリケーションに類似したルールで定義できます。case
たとえば、に(\x -> <expr1>) `seq` <expr2>
減少し<expr2>
ます。case
これは、アプリケーションと同じ方法で「評価を強制」します。WHNFは、これらの表現が「評価を強制する」ことの単なる名前です。
関数適用のニュアンスを説明してください:それはどのように厳格ですか?どうですか?
精査するのと同じように、左の表現case
も厳密です。case
置換後の選択された選択肢のRHSと同様に、置換後の関数本体でも厳密です。
どうdeepseq
ですか?
これは単なるライブラリ関数であり、組み込み関数ではありません。
ちなみに、deepseq
意味的に奇妙です。引数は1つだけにする必要があります。それを発明した人は誰でも、なぜ2つの議論が必要なseq
のかを理解せずに、盲目的にコピーしただけだと思います。私は、Haskellの評価についての理解が不十分であることが、経験豊富なHaskellプログラマーの間でも一般的であることの証拠として、の名前と仕様seq
を数えます。deepseq
let
とcase
ステートメント?
私はについて話しましcase
た。let
、脱糖と型チェックの後、ツリー形式で任意の式グラフを作成する方法にすぎません。これについての論文があります。
unsafePerformIO
?
ある程度、それは削減ルールによって定義することができます。たとえば、にcase unsafePerformIO <expr> of <alts>
減少しunsafePerformIO (<expr> >>= \x -> return (case x of <alts>))
、最上位レベルでのみ、にunsafePerformIO <expr>
減少し<expr>
ます。
これはメモ化を行いません。unsafePerformIO
すべての式を書き直して明示的にメモ化し、関連するIORef
s ...をどこかに作成することで、メモ化をシミュレートすることができます。ただし、GHCのメモ化動作は、最適化プロセスの予測できない詳細に依存し、タイプセーフでもないため(IORef
GHCドキュメントの悪名高い多態的な例で示されているように)、再現することはできません。
Debug.Trace
?
Debug.Trace.trace
の単純なラッパーunsafePerformIO
です。
トップレベルの定義?
トップレベルの変数バインディングは、ネストされたバインディングと同じlet
です。data
、、、class
などimport
はまったく別の球技です。
厳密なデータ型?強打パターン?
砂糖だけseq
。