IOモナドは技術的に間違っていますか?


12

haskell wikiには、IOモナドの条件付き使用の次の例があります(こちらを参照)

when :: Bool -> IO () -> IO ()
when condition action world =
    if condition
      then action world
      else ((), world)

この例では、の定義はすべてをより理解しやすくするためのものであるIO aことRealWorld -> (a, RealWorld)に注意してください。

このスニペットは、IOモナドで条件付きでアクションを実行します。さて、それconditionFalseであると仮定すると、アクションactionは決して実行されるべきではありません。遅延セマンティクスを使用すると、これが実際に当てはまります。ただし、ここではHaskellが技術的に厳密ではないことに注意してください。これは、たとえば、コンパイラーがaction world別のスレッドでプリエンプティブに実行することを許可され、後で必要でないことが判明したときにその計算を破棄することを意味します。ただし、その時点までに副作用はすでに発生しています。

さて、プログラム全体が終了したときにのみ副作用が伝播されるようにIOモナドを実装し、どの副作用を実行すべきかを正確に知ることができます。ただし、これはそうではありません。Haskellで無限のプログラムを作成することが可能であるため、明らかに中間の副作用があります。

これは、IOモナドが技術的に間違っていることを意味しますか、またはこれを防ぐ他の何かがありますか?


コンピュータサイエンスへようこそ!ここでの質問はトピック外です。プログラミングに関する質問ではなく、コンピューターサイエンスの質問を扱います(FAQを参照)。あなたの質問はStack Overflowで話題になっているかもしれません。
dkaeae

2
私の意見では、これはコンピューターサイエンスの質問です。なぜなら、実際のプログラミングの質問ではなく、Haskellの理論的なセマンティクスを扱っているからです。
ラッセ

4
私はプログラミング言語の理論にあまり詳しくありませんが、この質問はここで話題になっていると思います。ここで「間違っている」とはどういう意味かを明確にすると役立つ場合があります。IOモナドにはあるべきではないという特性があると思いますか?
離散トカゲ

1
このプログラムは適切に型付けされていません。あなたが実際に何を書くつもりなのか分かりません。の定義whenは入力可能ですが、宣言した型を持たないため、この特定のコードが興味深いのはわかりません。
ジル 'SO-悪であるのをやめる'

2
このプログラムは、直接リンクされているHaskell-wikiページから逐語的に取られています。それは確かに入力しません。これは、IOの内部をより読みやすくするために、IO aと定義された仮定の下で記述されRealWorld -> (a, RealWorld)ているためです。
ラッセ

回答:


12

これは、IOモナドの「解釈」として提案されています。この「解釈」を真剣に受け止めたい場合は、「RealWorld」を真剣に受け止める必要があります。action world投機的に評価されるかどうか、action副作用がないかどうかは関係ありません。その影響は、ネットワークパケットが送信されたなど、それらの影響が発生したユニバースの新しい状態を返すことによって処理されます。ただし、関数の結果((),world)worldです。したがって、宇宙の新しい状態はです。側で投機的に評価したかもしれない新しい宇宙を使用しません。宇宙の状態はworldです。

あなたはおそらくそれを真剣に考えるのに苦労しています。これはせいぜい表面的に逆説的で無意味な多くの方法があります。この観点では、同時実行性は特に非自明または狂気のどちらかです。

「待って、待って」とあなたは言う。「RealWorld単なる「トークン」です。実際には、宇宙全体の状態ではありません。」さて、この「解釈」は何も説明しません。それでも、実装の詳細として、これはGHCがモデル化する方法IOです。1ただし、これは、実際には副作用がある魔法の「機能」があることを意味し、このモデルはそれらの意味に対するガイダンスを提供しません。また、これらの関数には実際に副作用があるため、提起する懸念は完全に適切です。GHC 確実に邪魔にならないようになければならずRealWorld、これらの特別な機能は、プログラムの意図された動作を変更する方法で最適化されていません。

個人的には(おそらく今では明らかなように)、この「世界を通過する」モデルはIO、教育ツールとしては役に立たず、混乱を招くと思います。(実装に役立つかどうかはわかりません。GHCの場合、これはより歴史的な成果物だと思います。)

別のアプローチの1つはIO、応答ハンドラーを使用して要求を説明するものと見なすことです。これを行うにはいくつかの方法があります。おそらく最もアクセスしやすいのは、無料のモナド構造を使用することです。具体的には、次のものを使用できます。

data IO a = Return a | Request OSRequest (OSResponse -> IO a)

これをより洗練されたプロパティにするための多くの方法がありますが、これはすでに改善されています。理解するために、現実の性質についての深い哲学的仮定を必要としません。それが述べているのIOReturn、値を返す以外の何もしない単純なプログラムであるか、または応答のハンドラーを使用したオペレーティングシステムへの要求であるということだけです。OSRequest次のようになります:

data OSRequest = OpenFile FilePath | PutStr String | ...

同様に、OSResponse次のようになります。

data OSResponse = Errno Int | OpenSucceeded Handle | ...

(可能な改良点の1つは、リクエストOpenSucceededから取得しないことを確認できるように、よりタイプセーフにすることですPutStr。)このモデルIOは、何らかのシステムによって解釈されるリクエストを記述するものとしてモデル化されます(「実際の」IOモナドHaskellランタイム自体)、そしておそらく、そのシステムは、応答を提供したハンドラーを呼び出します。もちろん、これはリクエストのPutStr "hello world"処理方法を示すものではありませんが、ふりをするわけでもありません。これが他のシステムに委任されていることを明示します。このモデルもかなり正確です。最新のOSのすべてのユーザープログラムは、OSに何かを要求する必要があります。

このモデルは正しい直観を提供します。例えば、多くの初心者はのようなものを表示<-「アンラップ」としてオペレータをIO、またはこと(残念ながら強化)景色を眺めることができIO String、たとえば、「含まれている」ことを「コンテナ」であるStringS(そして<-それらを取得します)。この要求/応答ビューは、この観点を明らかに間違っています。内にファイルハンドルはありませんOpenFile "foo" (\r -> ...)。これを強調する一般的な例えは、ケーキのレシピの中にケーキがないことです(または、この場合は「請求書」が良いでしょう)。

このモデルは、並行性でも容易に機能します。OSRequestlikeのコンストラクターを簡単に作成Fork :: (OSResponse -> IO ()) -> OSRequestでき、ランタイムはこの追加ハンドラーによって生成された要求を通常のハンドラーとインターリーブできます。ある程度の賢明さで、この(または関連する手法)を使用して、「OSにリクエストを送信し、物事が発生する」だけでなく、並行性などをより直接モデル化できます。これがIOSpecライブラリの仕組みです。

1 Hugsは継続ベースの実装を使用しましたが、IOその実装は、明示的なデータ型ではなく不透明な関数を使用して説明したものとほぼ同じです。HBCは、古いリクエスト/レスポンスストリームベースのIOの上に階層化された継続ベースの実装も使用しました。NHC(したがってYHC)はサンクを使用しました。つまり、大まかに呼ばれてIO a = () -> aいました()World、状態の受け渡しはしていません。JHCとUHCは基本的にGHCと同じアプローチを使用しました。


あなたの啓発的な答えをありがとう、それは本当に役立った。IOの実装は私の心を包むのに時間がかかりましたが、より直感的であることに同意します。この実装は、RealWorld実装のように副作用の順序付けに関する潜在的な問題に悩まされていないと主張していますか?すぐに問題を確認することはできませんが、それらが存在しないことも明確ではありません。
ラッセ

一つのコメント:それOpenFile "foo" (\r -> ...)は実際にあるべきだと思われるRequest (OpenFile "foo") (\r -> ...)
ラッセ

@Lasseうん、それがあったはずRequestです。最初の質問に答えるために、これIOは不活性な値であるため、評価順序(モジュロボトム)に明らかに鈍感です。すべての副作用(もしあれば)は、この値を解釈するものによって引き起こされます。このwhen例では、action評価されたかどうかは関係ありません。Request (PutStr "foo") (...)これは、これらのリクエストを解釈するものに与えないような値になるからです。それはソースコードのようなものです。あなたがそれを熱心にまたは怠lyに減らすかどうかは関係ありません、インタプリタに与えられるまで何も起こりません。
デレクエルキンズは、

ああそうだね。これは本当に賢い定義です。最初は、データ構造を解釈する前にデータ構造を構築する必要があるため、プログラム全体の実行が終了したときにすべての副作用が必ず発生する必要があると考えました。ただし、リクエストには継続が含まれているため、Request副作用の発生を開始するには、最初のデータのみを構築する必要があります。継続を評価するときに、後続の副作用を作成できます。賢い!
ラッセ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.