Haskellで評価される関数a->()にはどのような規則がありますか?


12

タイトルが言うように:評価されるユニットを返すHaskell関数にはどのような保証がありますか?そのような場合、いかなる種類の評価も実行する必要はないと考えられ()ますが、厳密さの明示的な要求が存在しない限り、コンパイラーはそのようなすべての呼び出しを即時値に置き換えることができます。リターン()またはボトム。
私はこれをGHCiで実験しましたが、逆のことが起こっているようです。つまり、そのような関数は評価されているように見えます。非常に原始的な例は

f :: a -> ()
f _ = undefined

評価f 1すると、が存在するためにエラーがスローされるundefinedため、評価が確実に行われます。ただし、評価の深さは明確ではありません。場合によっては、を返す関数へのすべての呼び出しを評価する必要があるほど深くなるように見えることがあり()ます。例:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

このコードは、を指定すると無限にループしg (let x = 1:x in x)ます。しかしその後

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

を使用して、をh (f 1)返すことを示すことができる()ため、この場合、すべての単位値の部分式が評価されるわけではありません。ここでの一般的なルールは何ですか?

ETA:もちろん私は怠惰について知っています。コンパイラー作成者がこの特定のケースを通常よりもさらに遅延させることができないのはなぜですか。

ETA2:例の要約:GHCは()他の型とまったく同じように扱われるように見えます。つまり、関数から型に存在する通常の値を返す必要があるかどうかの質問があるかのように見えます。そのような値が1つしかないという事実は、最適化アルゴリズムによって(ab)使用されていないようです。

ETA3:Haskellと言うとき、Haskell-the-H-in-GHCではなく、Haskell-as-the-defined-the-Reportを意味します。私が想像したほど広く共有されていない仮定(「読者の100%による」)であると思われるか、またはより明確な質問を定式化できただろう。それでも、最初はそのような関数が呼び出されることについてどのような保証があるかを尋ねていたため、質問のタイトルを変更したことを後悔しています。

ETA4:この質問は当然のように実行されたように思われ、私はそれを未回答と考えています。(「近い質問」機能を探していましたが、「自分の質問に答える」しか見つかりませんでした。答えられないので、そのルートをたどりませんでした)これは、強力であるが明確ではない「そのような言語に対する保証はない」という答えとして解釈したくなります。私たちが知っているのは、現在のGHC実装はそのような関数の評価をスキップしないということです。

OCamlアプリをHaskellに移植するときに実際の問題に遭遇しました。元のアプリには多くのタイプの相互再帰構造があり、コードはassert_structureN_is_correct1..6または7でNを呼び出すいくつかの関数を宣言しました。それぞれが構造が正しい場合にユニットを返し、正しくない場合に例外をスローしました。さらに、これらの関数は正当性の条件を分解するため、相互に呼び出されました。Haskellでは、これはEither Stringモナドを使用してより適切に処理されるため、私はそれをそのように転記しましたが、理論的な問題としての質問は残りました。すべての入力と返信をありがとう。


1
これは仕事での怠惰です。関数の結果が要求されない限り(例えば、コンストラクターに対するパターンマッチングによって)、関数の本体は評価されません。違いを観察するには、とを比較h1::()->() ; h1 () = ()してみてくださいh2::()->() ; h2 _ = ()。との両方h1 (f 1)を実行しh2 (f 1)、最初の1つだけが要求することを確認し(f 1)ます。
チー

1
「怠惰は、それが()に置き換えられて、なんらかの評価が行われないことを要求するように思われるでしょう。」どういう意味ですか?すべての場合でf 1「置き換え」られundefinedます。
oisdk

3
関数... -> ()は、1)終了して戻る()、2)例外/実行時エラーで終了して何も返さない、または3)分岐する(無限再帰)ことができます。GHCは、1)のみが発生する可能性があると想定してコードを最適化しません。f 1要求された場合、評価をスキップしてリターンしません()。Haskellのセマンティクスは、それを評価し、1,2,3の間で何が起こるかを確認することです。
カイ

2
()この質問では、(タイプと値のどちらについても)特別なことは何もありません。() :: ()たとえば、0 :: Intどこにでも入れ替えると、同じことがすべて起こります。これらはすべて、遅延評価の古い退屈な結果です。
Daniel Wagner

2
いいえ、「回避」などはHaskellのセマンティクスではありません。また、type との2つの可能な値があります。()()undefined
Will Ness、

回答:


10

あなたはタイプがあるという仮定から来たように見える()、唯一の可能な値を持っている()ので、期待していることを任意の型の値を返す関数呼び出しが()自動的に実際に値を生成すると仮定しなければなりません()

これはHaskellの動作方法ではありません。Haskellにはすべての型の値が1つあります。つまり値、エラー、いわゆる「ボトム」があり、によってエンコードされundefinedます。したがって、実際に評価が行われています。

main = print (f 1)

コア言語の

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

または(*)

main = _Case (f 1) _Of x -> putStr "()"

コア_Case強制している

%case[式]を評価すると、テスト対象の式(「調査対象」)が強制的に評価されます。調査対象の値は、%ofキーワード...に続く変数にバインドされます。

値は弱い頭の通常の形式に強制されます。これは言語定義の一部です。

Haskellはありません宣言型プログラミング言語。


(*) print x = putStr (show x)show () = "()"なので、show呼び出しをまとめてコンパイルできます。

値は確かに事前にとして知られており()、の値でさえもshow ()事前にとして知られてい"()"ます。それでも、受け入れられているHaskellのセマンティクスで(f 1)は、事前に既知の文字列を出力する前に、の値を弱いヘッドの通常形式にすることを要求しています"()"


編集:を検討してくださいconcat (repeat [])。であるべきか[]、それとも無限ループであるべきか?

これに対する「宣言型言語」の答えはおそらく[]です。Haskellの答えは、無限ループです。

怠惰「貧乏人の宣言型プログラミング」ですが、それでもまだ本物ではありません。

edit2:強制されるprint $ h (f 1) == _Case (h (f 1)) _Of () -> print ()だけhで、強制されませんf。その定義によれば、その答えを出すためにh何も強制する必要はありませんh _ = ()

別れの言葉:怠惰には存在意義があるかもしれませんが、それはその定義ではありません。怠惰はそれが何であるかです。これは、からの要求に応じてWHNFに強制される最初のサンクであるすべての値として定義されますmain。特定の状況に応じて特定のケースでボトムを回避するのに役立つ場合は、回避します。そうでない場合、そうではありません。以上です。

それを自分の好きな言語で実装して、感触をつかむのに役立ちます。ただし、すべての暫定値の名前を慎重に指定することで、式の評価を追跡できます


行くレポート、我々は持っています

f :: a -> ()
f = \_ -> (undefined :: ())

その後

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

続く

 = putStrLn (case (undefined :: ()) of () -> "()")

今、セクション3.17.3レポートのパターンマッチングの正式な意味論は言う

case図3.1–3.3に、式の意味を示します。実装は、これらのIDが[...]を保持するように動作する必要があります。

ケース(r)図3.2の状態

(r)     case  of { K x1  xn -> e; _ -> e } =  
        where K is a data constructor of arity n 

() アリティ0のデータコンストラクタなので、次と同じです。

(r)     case  of { () -> e; _ -> e } =  

したがって、評価の全体的な結果はです。


2
私はあなたの説明が好きです。それは明確でシンプルです。
arrowd

@DanielWagner私はcase実際にはCoreからのものを念頭に置いており、大きな穴を無視していました。:)コアについて言及するために編集しました。
Will Ness、

1
強制はshow呼び出されprintませんか?次のようなものshow x = case x of () -> "()"
user253751

1
私はcaseHaskell自体ではなく、Coreで言及します。Haskellは、強制的なcaseAFAIK を持つCoreに翻訳されます。あなたはcaseHaskellでそれ自体が強制的ではないことは正しいです。私は、SchemeやML(MLを記述できる場合)、または疑似コードで何かを記述できます。
Will Ness、

1
これらすべてに対する信頼できる回答は、おそらくレポートのどこかにあります。私が知っているのは、ここでは「最適化」が行われておらず、「通常の値」はこの文脈では意味のある用語ではないということです。強制されるものは何でも、強制される。print印刷に必要なだけの力を加えます。プログラムはタイプを調べません。プログラムが実行されるまでに、タイプはなくなり、消去されます。正しい印刷サブルーチンがすでに選択されており、タイプに応じてコンパイル時にコンパイルされます。そのサブルーチンは、実行時に入力値をWHNFに強制するため、未定義の場合はエラーが発生します。
Will Ness
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.