Haskellがデフォルトで怠惰であることは誰もが知っています(または知っておくべきです)。評価する必要があるまで、何も評価されません。では、いつ何かを評価する必要がありますか? Haskellが厳格でなければならない点があります。私はこれらを「厳密点」と呼んでいますが、この特定の用語は私が思っていたほど広範ではありません。私によると:
Haskellonlyの削減(または評価)は、厳密点で発生します。
したがって、質問は次のとおりです。何、正確に、ハスケルの厳密さのポイントは何ですか?私の直感ではmain
、 seq
/bangパターン、パターンマッチング、およびIO
を介して実行されるmain
アクションが主要な厳密点ですが、なぜそれを知っているのかよくわかりません。
(また、それらが「厳密点」と呼ばれていない場合、それらは何と呼ばれますか?)
良い答えには、WHNFなどについての議論が含まれると思います。また、ラムダ計算に触れる可能性があると思います。
編集:この質問に関する追加の考え
この質問を振り返ったように、厳密性ポイントの定義に何かを追加する方が明確だと思います。厳密性ポイントは、さまざまなコンテキストとさまざまな深さ(または厳密さ)を持つことができます。 「Haskellの削減は厳密性ポイントでのみ発生する」という私の定義に戻って、この句をその定義に追加しましょう。「厳密性ポイントは、周囲のコンテキストが評価または削減されたときにのみトリガーされます。」
だから、私が望む種類の答えをあなたに始めさせようと思います。 main
は厳密さのポイントです。それは、その文脈の主要な厳格さのポイントとして特別に指定されています:プログラム。プログラム(main
のコンテキスト)が評価されると、mainの厳密性ポイントがアクティブになります。メインの深さは最大です:それは完全に評価されなければなりません。 Mainは通常、IOアクションで構成されます。これは厳密性ポイントでもあり、そのコンテキストはmain
です。
次に、これらの用語でseq
とパターンマッチングについて説明します。関数適用のニュアンスを説明してください:それはどのように厳密ですか?どうですか? deepseq
はどうですか? let
およびcase
ステートメント? unsafePerformIO
? Debug.Trace
?トップレベルの定義?厳密なデータ型?強打パターン?などこれらの項目のうち、シーケンスまたはパターンマッチングだけで説明できるものはいくつありますか?
開始するのに適した場所は、このペーパーを理解することです。 遅延評価のための自然なセマンティクス (ローンチブリー)。これにより、GHCのコアに似た小さな言語で式が評価される時期がわかります。次に、残りの質問は、完全なHaskellをCoreにマップする方法であり、その翻訳のほとんどはHaskellレポート自体によって提供されます。 GHCでは、このプロセスを「脱糖」と呼びます。これは、糖衣を除去するためです。
GHCには、脱糖とコード生成の間の最適化が多数含まれているため、これですべてではありません。これらの変換の多くは、コアを再配置して、物事が異なる時間に評価されるようにします(特に、厳密性分析により、物事が評価されます)。ついさっき)。したがって、yourプログラムがどのように評価されるかを実際に理解するには、GHCによって生成されたコアを調べる必要があります。
おそらくこの答えはあなたには少し抽象的なように思えますが(私はバングパターンやシーケンスについては特に言及しませんでした)、あなたは何かpreciseを求めました、そしてこれは私たちができる最善のことについて。
私はおそらくこの質問を次のように書き直しますハスケルはどのような状況で式を評価しますか?(おそらく「頭が弱い通常の形」に取り組む)
最初の概算として、これを次のように指定できます。
直感的なリストから、mainアクションとIOアクションは最初のカテゴリに分類され、seqとパターンマッチングは2番目のカテゴリに分類されます。しかし、最初のカテゴリはあなたの考えに沿っていると思います。 「厳密さのポイント」、それが実際にHaskellでの評価をユーザーにとって観察可能効果にする方法だからです。
Haskellは大きな言語であるため、すべての詳細を具体的に示すことは大きな作業です。また、Concurrent Haskellは、最終的に結果を使用しなくなったとしても、物事を投機的に評価する可能性があるため、非常に微妙です。これは、評価を引き起こす3番目の種類の物です。 2番目のカテゴリは非常によく研究されています。関連する関数のstrictnessを調べたいと思います。最初のカテゴリも一種の「厳密さ」と考えることができますが、_evaluate x
_とseq x $ return ()
は実際には異なるため、これは少し危険です。 IOモナド(単純なケースでは明示的に_RealWorld#
_トークンを渡す)に何らかのセマンティクスを与えると、適切に処理できます)が、あるかどうかはわかりません。一般に、この種の層化された厳密性分析の名前。
Cには シーケンスポイント の概念があります。これは、特定の演算に対して、一方のオペランドがもう一方のオペランドよりも先に評価されることを保証するものです。これが最も近い既存の概念だと思いますが、本質的に同等の用語strictness point(またはおそらくforce point)はより一致していますHaskellの考え方。
実際には、Haskellは純粋に怠惰な言語ではありません。たとえば、パターンマッチングは通常厳密です(したがって、パターンマッチングを試みると、少なくとも一致を受け入れるか拒否するのに十分な距離まで評価が行われるようになります。
…
プログラマーは、
seq
プリミティブを使用して、結果が使用されるかどうかに関係なく、式に評価を強制することもできます。
$!
はseq
で定義されます。— レイジーvs.非厳密 。
したがって、!
/$!
とseq
についてのあなたの考えは本質的に正しいですが、パターンマッチングはより微妙なルールに従う必要があります。もちろん、いつでも~
を使用してレイジーパターンマッチングを強制できます。その同じ記事からの興味深い点:
厳密性アナライザーは、部分式が常に外部式によって必要とされる場合も探し、それらを先行評価に変換します。セマンティクス(「ボトム」に関して)が変更されないため、これを行うことができます。
うさぎの穴を続けて、GHCによって実行される最適化のドキュメントを見てみましょう。
厳密性分析は、GHCがコンパイル時に、どのデータが「常に必要」であるかを確実に判断しようとするプロセスです。 GHCは、計算を保存して後で実行するための通常の(オーバーヘッドが高い)プロセスではなく、そのようなデータを計算するだけのコードを作成できます。
— GHC最適化:正格性解析 。
言い換えると、厳密なコードは最適化としてどこでも生成される可能性があります。データが常に必要な場合(および/または一度しか使用されない場合)にサンクを作成すると不必要にコストがかかるためです。
…値に対してこれ以上評価を実行することはできません。 通常の形式であると言われています。値に対して少なくともいくつかの評価を実行するように中間ステップのいずれかにいる場合、それは弱い頭の通常の形式(WHNF )。 (「ヘッドノーマルフォーム」もありますが、Haskellでは使用されていません。)WHNFで何かを完全に評価すると、通常のフォームになります…
(頭の位置にベータレデックスがない場合、用語は頭の正規形になります1。非レデックスのラムダアブストラクタのみが先行する場合、レデックスはヘッドリデックスです。 2。)つまり、サンクを強制し始めると、WHNFで作業していることになります。強制するサンクがなくなると、通常の状態になります。もう1つの興味深い点:
…ある時点で、たとえばユーザーにzを印刷する必要がある場合は、それを完全に評価する必要があります…
これは当然、IO
から実行されるすべてのmain
アクションが強制評価を行うことを意味します。これは、Haskellプログラムが行うことを考慮すると明らかです。実際、物事を行います。 main
で定義されたシーケンスを通過する必要があるものはすべて、通常の形式である必要があるため、厳密な評価の対象となります。
C. A. McCannはコメントでそれを正しく理解しました:main
について特別なことは、main
が特別なものとして定義されていることだけです。コンストラクターでのパターンマッチングは、IO
モナドによって課されるシーケンスを保証するのに十分です。その点では、seq
とパターンマッチングのみが基本です。
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の操作的意味論の専門家ではないので、リンクをリソースとして残しておきます。
その他のリソース:
怠惰は何もしないという意味ではありません。プログラムパターンがcase
式に一致するときはいつでも、何かを評価します-とにかく十分です。そうしないと、どのRHSを使用するかがわかりません。コードに大文字と小文字の表現がありませんか?心配しないでください。コンパイラーは、コードをHaskellの簡略化された形式に変換しているため、使用を避けるのは困難です。
初心者の場合、基本的な経験則はlet
は怠惰であり、case
は怠惰ではありません。
これはカルマを目的とした完全な答えではありませんが、パズルの一部にすぎません-これがセマンティクスに関するものである限り、同じを提供する複数の評価戦略があることに注意してくださいセマンティクス。ここでの1つの良い例-そしてこのプロジェクトは、私たちが通常Haskellのセマンティクスをどのように考えるかについても語っています-は、維持同じセマンティクス: http://csg.csail.mit.edu/pubs/haskell.html
Glasgow Haskellコンパイラは、コードをcoreと呼ばれるラムダ計算のような言語に変換します。この言語では、case
-ステートメントでパターンマッチングするたびに、何かが評価されます。したがって、関数が呼び出されると、最も外側のコンストラクターとそれだけ(強制フィールドがない場合)が評価されます。それ以外はサンクに缶詰にされます。 (サンクはlet
バインディングによって導入されます)。
もちろん、これは実際の言語で起こることとまったく同じではありません。コンパイラーは非常に洗練された方法でHaskellをCoreに変換し、可能な限り多くのものを怠惰にし、常に怠惰に必要なものをすべて作成します。さらに、ボックス化されていない値と、常に厳密なタプルがあります。
関数を手作業で評価しようとすると、基本的に次のように考えることができます。