92

Haskellがデフォルトで怠惰であることは誰もが知っています(または知っておくべきです)。評価する必要があるまで、何も評価されません。では、いつ何かを評価する必要がありますか?Haskellが厳格でなければならない点があります。私はこれらを「厳密さのポイント」と呼んでいますが、この特定の用語は私が思っていたほど広くはありません。私によると:

Haskellの削減(または評価)は、厳密なポイントでのみ発生します。

したがって、問題は、ハスケルの厳格さのポイントは正確には何ですか?私の直感ではmainseq/ bangパターン、パターンマッチング、およびIOを介して実行されるアクションmainが主要な厳密点であると言われていますが、なぜそれを知っているのかはよくわかりません。

(また、それらが「厳密点」と呼ばれない場合、それらは何呼ばれますか?)

良い答えには、WHNFなどについての議論が含まれると思います。また、ラムダ計算に触れる可能性があると思います。


編集:この質問についての追加の考え。

この質問を振り返ったように、厳密性ポイントの定義に何かを追加する方が明確だと思います。厳密性ポイントは、さまざまなコンテキストとさまざまな深さ(または厳密さ)を持つことができます。「Haskellでの削減は厳密性ポイントでのみ発生する」という私の定義に戻って、この節を追加しましょう。「厳密性ポイントは、周囲のコンテキストが評価または削減されたときにのみトリガーされます。」

だから、私が望む種類の答えをあなたに始めさせようと思います。main厳しさのポイントです。これは、そのコンテキストの主要な厳密点であるプログラムとして特別に指定されています。プログラム(mainのコンテキスト)が評価されると、mainの厳密点がアクティブになります。メインの深さは最大です:それは完全に評価されなければなりません。Mainは通常、IOアクションで構成されます。これは、コンテキストがである厳密ポイントでもありますmain

今、あなたは試してみます:seqこれらの用語で議論し、パターンマッチングします。関数適用のニュアンスを説明してください:それはどのように厳格ですか?どうですか?どうdeepseqですか?letcaseステートメント?unsafePerformIODebug.Trace?トップレベルの定義?厳密なデータ型?強打パターン?などこれらのアイテムのうち、シーケンスまたはパターンマッチングの観点から説明できるものはいくつありますか?

4

8 に答える 8

48

開始するのに適した場所は、このペーパーを理解することです:怠惰な評価のための自然な意味論(Launchbury)。これにより、GHCのコアに似た小さな言語で式が評価される時期がわかります。次に、残りの質問は、完全なHaskellをCoreにマップする方法であり、その変換のほとんどは、Haskellレポート自体によって提供されます。GHCでは、このプロセスを「脱糖」と呼びます。これは、糖衣を除去するためです。

GHCには、脱糖とコード生成の間の最適化が多数含まれているため、これですべてではありません。これらの変換の多くは、コアを再配置して、さまざまな時点で評価されるようにします(特に、厳密性分析により、評価が行われます)。ついさっき)。したがって、プログラムがどのように評価されるかを実際に理解する には、GHCによって作成されたコアを調べる必要があります。

おそらくこの答えはあなたには少し抽象的なように思えますが(私はバングパターンやシーケンスについては特に言及しませんでした)、あなたは正確な何かを求めました、そしてこれは私たちができる最善のことです。

于 2011-09-20T22:49:27.850 に答える
20

私はおそらくこの質問を次のように書き直します。ハスケルはどのような状況で式を評価しますか?(おそらく「頭が弱い通常の形」に取り組む。)

最初の概算として、これを次のように指定できます。

  • IOアクションを実行すると、「必要な」式が評価されます。(したがって、IOアクションが実行されているかどうかを知る必要があります。たとえば、名前がmainであるか、mainから呼び出されているか、アクションに必要なものを知る必要があります。)
  • 評価されている式(これは再帰的定義です!)は、必要な式を評価します。

直感的なリストから、メインアクションとIOアクションは最初のカテゴリに分類され、seqとパターンマッチングは2番目のカテゴリに分類されます。しかし、最初のカテゴリーは、「厳密点」というあなたの考えにもっと一致していると思います。それは、実際、Haskellでの評価をユーザーにとって観察可能な効果にする方法だからです。

Haskellは大きな言語であるため、すべての詳細を具体的に示すことは大きな作業です。また、Concurrent Haskellは、最終的に結果を使用しなくなったとしても、物事を投機的に評価する可能性があるため、非常に微妙です。これは、評価を引き起こす3番目の種類の物です。2番目のカテゴリは非常によく研究されています。関連する関数の厳密さを確認する必要があります。最初のカテゴリも一種の「厳密さ」と考えることができますが、これは少し危険です。実際evaluate xseq x $ return ()は異なるものだからです。IOモナドにある種のセマンティクスを与えると適切に扱うことができます(RealWorld#トークンを明示的に渡すことは単純な場合に機能します)が、一般にこの種の層化された正格性解析の名前があるかどうかはわかりません。

于 2011-09-20T21:24:21.743 に答える
18

Cには、シーケンスポイントの概念があります。これは、特定の演算に対して、一方のオペランドがもう一方のオペランドよりも先に評価されることを保証するものです。これが最も近い既存の概念だと思いますが、本質的に同等の用語である厳密性ポイント(またはおそらく強制ポイント)は、Haskellの考え方とより一致しています。

実際には、Haskellは純粋に怠惰な言語ではありません。たとえば、パターンマッチングは通常厳密です(したがって、パターンマッチングを試みると、少なくとも一致を受け入れるか拒否するのに十分な距離で評価が行われるようになります。

…</p>

プログラマーは、seqプリミティブを使用して、結果が使用されるかどうかに関係なく、式に評価を強制することもできます。

$!で定義されseqます。

— <ahref="http://www.haskell.org/haskellwiki/Lazy_vs._non-strict"rel="noreferrer">レイジーvs.ノンストリクト。

したがって、!/についてのあなたの考えは本質的に正しいですが、パターンマッチングはより微妙なルールに従う必要があります。もちろん、怠惰なパターンマッチングを強制するためにいつでも使用できます。その同じ記事からの興味深い点:$!seq~

厳密性アナライザーは、部分式が常に外部式で必要とされる場合も探し、それらを先行評価に変換します。セマンティクス(「ボトム」の観点から)が変更されないため、これを行うことができます。

うさぎの穴を続けて、GHCによって実行される最適化のドキュメントを見てみましょう。

正格性解析は、GHCがコンパイル時に、どのデータが「常に必要」であるかを確実に判断しようとするプロセスです。GHCは、計算を保存して後で実行するための通常の(オーバーヘッドが高い)プロセスではなく、そのようなデータを計算するだけのコードを作成できます。

— <a href="http://www.haskell.org/haskellwiki/GHC_optimisations#Strictness_analysis" rel="noreferrer"> GHC最適化:厳密性分析。

言い換えると、データが常に必要になる場合(および/または一度だけ使用される場合)にサンクを作成すると不必要にコストがかかるため、厳密なコードは最適化としてどこでも生成できます。

…値に対してこれ以上評価を実行することはできません。正規形と言われています。値に対して少なくともある程度の評価を実行するために中間ステップのいずれかにいる場合、それはウィークヘッドノーマルフォーム(WHNF)です。(「ヘッド正規形」もありますが、Haskellでは使用されていません。)WHNFで何かを完全に評価すると、通常の形になります…</ p>

— <a href="http://en.wikibooks.org/wiki/Haskell/Laziness" rel="noreferrer">ウィキブックスHaskell:怠惰

(ヘッド位置1にベータレデックスがない場合、用語はヘッド正規形になります。非レデックス2のラムダアブストラクタのみが前にある場合、レデックスはヘッドレデックスです。)したがって、サンクを強制し始めると、あなたはWHNFで働いています。強制するサンクがなくなると、通常の状態になります。もう1つの興味深い点:

…ある時点で、たとえばユーザーにzを印刷する必要がある場合は、それを完全に評価する必要があります…</ p>

これは当然、IOから実行されるすべてのアクションmain 強制評価を行うことを意味します。これは、Haskellプログラムが実際に何かを行うことを考えると明らかなはずです。で定義されたシーケンスを通過するmain必要があるものはすべて正規形である必要があるため、厳密な評価の対象となります。

ただし、CA McCannはコメントでそれを正しく理解しました。特別なのは、特別なものとして定義されていることだけmainですmainIOコンストラクターでのパターンマッチングは、モナドによって課されるシーケンスを保証するのに十分です。その点でのみseq、パターンマッチングが基本です。

于 2011-09-20T21:41:21.837 に答える
10

HaskellはAFAIKであり、純粋な怠惰な言語ではなく、厳密ではない言語です。これは、必ずしも最後の可能な瞬間に用語を評価するわけではないことを意味します。

Haskellの「怠惰」モデルの良い情報源はここにあります:http://en.wikibooks.org/wiki/Haskell/Laziness

基本的に、サンクとウィークヘッダーの正規形WHNFの違いを理解することが重要です。

私の理解では、haskellは命令型言語と比較して計算を逆方向に引き出します。これが意味するのは、「seq」とbangパターンがない場合、最終的にはサンクの評価を強制するある種の副作用であり、それが順番に事前評価を引き起こす可能性があるということです(真の怠惰)。

これはひどいスペースリークにつながるため、コンパイラは、スペースを節約するために、サンクを事前に評価する方法とタイミングを判断します。プログラマーは、厳密性の注釈(en.wikibooks.org/wiki/Haskell/Strictness、www.haskell.org/haskellwiki/Performance/Strictness)を付けてこのプロセスをサポートし、ネストされたサンクの形でスペースの使用量をさらに減らすことができます。

私はhaskellの操作的意味論の専門家ではないので、リンクをリソースとして残しておきます。

その他のリソース:

http://www.haskell.org/haskellwiki/Performance/Laziness

http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation

于 2011-09-20T21:13:00.153 に答える
6

怠惰は何もしないという意味ではありません。プログラムパターンがcase式と一致するときはいつでも、それは何かを評価します-とにかく十分です。そうしないと、どのRHSを使用するかがわかりません。コードに大文字と小文字の表現がありませんか?心配しないでください。コンパイラーはコードをHaskellの簡略化された形式に変換しているので、使用を避けるのは困難です。

初心者の場合、基本的な経験則letは怠惰であり、怠惰でcaseはありません。

于 2011-09-20T21:39:53.563 に答える
4

これはカルマを目的とした完全な答えではありませんが、パズルの一部にすぎません。これがセマンティクスに関するものである限り、同じセマンティクスを提供する複数の評価戦略があることに注意してください。ここでの良い例の1つは、このプロジェクトは、Haskellのセマンティクスを通常どのように考えるかについても語っています。これは、同じセマンティクスを維持しながら評価戦略を根本的に変更したEagerHaskellプロジェクトでした。http: //csg.csail.mit.edu/ pubs / haskell.html

于 2011-09-23T21:18:10.747 に答える
2

Glasgow Haskellコンパイラは、コードをコアと呼ばれるラムダ計算のような言語に変換します。この言語では、-ステートメントによってパターンマッチするたびに、何かが評価されますcase。したがって、関数が呼び出された場合、最も外側のコンストラクターとそれのみ(強制フィールドがない場合)が評価されます。それ以外のものはサンクに缶詰にされます。(サンクはバインディングによって導入されletます)。

もちろん、これは実際の言語で起こることとは正確には異なります。コンパイラーは非常に洗練された方法でHaskellをCoreに変換し、可能な限り多くのものを怠惰にし、常に怠惰に必要なものをすべて作成します。さらに、常に厳密なボックス化されていない値とタプルがあります。

関数を手作業で評価しようとすると、基本的に次のように考えることができます。

  • リターンの最も外側のコンストラクターを評価してみてください。
  • 結果を得るために他に何かが必要な場合(ただし、それが本当に必要な場合のみ)も評価されます。順序は関係ありません。
  • IOの場合、その最初から最後までのすべてのステートメントの結果を評価する必要があります。IOモナドは特定の順序で評価を強制するためにいくつかのトリックを実行するため、これはもう少し複雑です。
于 2011-09-26T13:40:50.340 に答える
0

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が実際にこのような最適化の機会を利用しているかどうかはわかりません。yIntfx+yf

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つに一致する状態。しかし、言語仕様では、それほど多くの言葉でそれを行うように指示されていません。TrueFalse<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

そのどれにも意味があるとは思いません。

メインの深さは最大です:それは完全に評価されなければなりません。

いいえ、mainI / Oアクションをトップレベルに表示するには、「浅く」評価する必要があります。mainはプログラム全体であり、すべてのコードがすべての実行に関連しているわけではないため、プログラムはすべての実行で完全に評価されるわけではありません(一般的に)。

これらの用語で話し合いseq、パターンマッチングを行います。

パターンマッチングについてはすでに話しました。seqアプリケーションに類似したルールで定義できます。caseたとえば、に(\x -> <expr1>) `seq` <expr2>減少し<expr2>ます。caseこれは、アプリケーションと同じ方法で「評価を強制」します。WHNFは、これらの表現が「評価を強制する」ことの単なる名前です。

関数適用のニュアンスを説明してください:それはどのように厳格ですか?どうですか?

精査するのと同じように、左の表現caseも厳密です。case置換後の選択された選択肢のRHSと同様に、置換後の関数本体でも厳密です。

どうdeepseqですか?

これは単なるライブラリ関数であり、組み込み関数ではありません。

ちなみに、deepseq意味的に奇妙です。引数は1つだけにする必要があります。それを発明した人は誰でも、なぜ2つの議論が必要なseqのかを理解せずに、盲目的にコピーしただけだと思います。私は、Haskellの評価についての理解が不十分であることが、経験豊富なHaskellプログラマーの間でも一般的であることの証拠として、の名前と仕様seqを数えます。deepseq

letcaseステートメント?

私はについて話しましcaseた。let、脱糖と型チェックの後、ツリー形式で任意の式グラフを作成する方法にすぎません。これについての論文があります。

unsafePerformIO

ある程度、それは削減ルールによって定義することができます。たとえば、にcase unsafePerformIO <expr> of <alts>減少しunsafePerformIO (<expr> >>= \x -> return (case x of <alts>))、最上位レベルでのみ、にunsafePerformIO <expr>減少し<expr>ます。

これはメモ化を行いません。unsafePerformIOすべての式を書き直して明示的にメモ化し、関連するIORefs ...をどこかに作成することで、メモ化をシミュレートすることができます。ただし、GHCのメモ化動作は、最適化プロセスの予測できない詳細に依存し、タイプセーフでもないため(IORefGHCドキュメントの悪名高い多態的な例で示されているように)、再現することはできません。

Debug.Trace

Debug.Trace.traceの単純なラッパーunsafePerformIOです。

トップレベルの定義?

トップレベルの変数バインディングは、ネストされたバインディングと同じletです。data、、、classなどimportはまったく別の球技です。

厳密なデータ型?強打パターン?

砂糖だけseq

于 2020-12-11T21:54:44.577 に答える