Haskellで副作用がモナドとしてモデル化されているのはなぜですか?


172

Haskellの純粋でない計算がモナドとしてモデル化されている理由について、誰かがいくつかの指針を示すことができますか?

つまり、モナドは4つの操作のインターフェースにすぎないので、モナドの副作用をモデル化する理由は何でしたか?


15
モナドは2つの演算を定義するだけです。
Dario

3
しかし、リターンとフェイルについてはどうですか?((>>)と(>> =)以外)
bodacydo 2010年

55
2つの操作はreturnおよび(>>=)です。x >> yと同じですx >>= \\_ -> y(つまり、最初の引数の結果は無視されます)。私達は話しませんfail
ポルジュ2010年

2
@Porges失敗について話しませんか?そのIEで多少役に立つかもしれない、パーサなど
代替

16
@monadic:failであるMonadため、歴史的な事故のクラス。それは本当に属していMonadPlusます。デフォルトの定義は安全ではないことに注意してください。
JB。

回答:


292

関数に副作用があるとします。生成するすべての効果を入力および出力パラメーターとして受け取る場合、関数は外界に対して純粋です。

だから、不純な機能のために

f' :: Int -> Int

RealWorldを検討に追加します

f :: Int -> RealWorld -> (Int, RealWorld)
-- input some states of the whole world,
-- modify the whole world because of the side effects,
-- then return the new world.

その後、f再び純粋です。パラメータ化されたデータ型を定義しているため、type IO a = RealWorld -> (a, RealWorld)RealWorldを何度も入力する必要はなく、次のように書くことができます。

f :: Int -> IO Int

プログラマーにとって、RealWorldを直接操作するのは危険すぎます。特に、プログラマーがタイプRealWorldの値を手に入れると、それをコピーしようとする可能性がありますが、これは基本的に不可能です。(たとえば、ファイルシステム全体をコピーしようとすることを考えてください。どこに配置しますか?)したがって、IOの定義は、全世界の状態もカプセル化します。

「純粋でない」関数の構成

これらの不純な関数は、一緒にチェーンできない場合は役に立ちません。検討する

getLine     :: IO String            ~            RealWorld -> (String, RealWorld)
getContents :: String -> IO String  ~  String -> RealWorld -> (String, RealWorld)
putStrLn    :: String -> IO ()      ~  String -> RealWorld -> ((),     RealWorld)

したい

  • コンソールからファイル名を取得し、
  • そのファイルを読み取り
  • 印刷コンソールにそのファイルの内容を。

現実の世界の州にアクセスできたらどうしますか?

printFile :: RealWorld -> ((), RealWorld)
printFile world0 = let (filename, world1) = getLine world0
                       (contents, world2) = (getContents filename) world1 
                   in  (putStrLn contents) world2 -- results in ((), world3)

ここにパターンがあります。関数は次のように呼び出されます:

...
(<result-of-f>, worldY) = f               worldX
(<result-of-g>, worldZ) = g <result-of-f> worldY
...

したがって、~~~それらをバインドする演算子を定義できます。

(~~~) :: (IO b) -> (b -> IO c) -> IO c

(~~~) ::      (RealWorld -> (b,   RealWorld))
      ->                    (b -> RealWorld -> (c, RealWorld))
      ->      (RealWorld                    -> (c, RealWorld))
(f ~~~ g) worldX = let (resF, worldY) = f worldX
                   in g resF worldY

その後、私たちは単に書くことができます

printFile = getLine ~~~ getContents ~~~ putStrLn

現実の世界に触れることなく。

「浄化」

次に、ファイルのコンテンツも大文字にしたいとします。大文字は純粋な関数です

upperCase :: String -> String

しかし、それを現実の世界にするためには、を返す必要がありIO Stringます。そのような関数を持ち上げるのは簡単です:

impureUpperCase :: String -> RealWorld -> (String, RealWorld)
impureUpperCase str world = (upperCase str, world)

これは一般化することができます:

impurify :: a -> IO a

impurify :: a -> RealWorld -> (a, RealWorld)
impurify a world = (a, world)

そのためimpureUpperCase = impurify . upperCase、そして私たちは書くことができます

printUpperCaseFile = 
    getLine ~~~ getContents ~~~ (impurify . upperCase) ~~~ putStrLn

(注:通常はと書きますgetLine ~~~ getContents ~~~ (putStrLn . upperCase)

私たちはずっとモナドを使っていました

では、何をしたか見てみましょう。

  1. (~~~) :: IO b -> (b -> IO c) -> IO c2つの不純な関数をつなぐ演算子を定義しました
  2. impurify :: a -> IO a純粋な値を不純に変換する関数を定義しました。

今、私たちは、識別を行う(>>=) = (~~~)return = impurifyし、参照してください?モナドを持っています。


テクニカルノート

それが本当にモナドであることを保証するために、チェックする必要があるいくつかの公理がまだあります:

  1. return a >>= f = f a

     impurify a                =  (\world -> (a, world))
    (impurify a ~~~ f) worldX  =  let (resF, worldY) = (\world -> (a, world )) worldX 
                                  in f resF worldY
                               =  let (resF, worldY) =            (a, worldX)       
                                  in f resF worldY
                               =  f a worldX
  2. f >>= return = f

    (f ~~~ impurify) worldX  =  let (resF, worldY) = f worldX 
                                in impurify resF worldY
                             =  let (resF, worldY) = f worldX      
                                in (resF, worldY)
                             =  f worldX
  3. f >>= (\x -> g x >>= h) = (f >>= g) >>= h

    運動のままに。


5
+1ですが、これは特にIOのケースを対象としています。 blog.sigfpe.com/2006/08/you-could-have-invented-monads-and.htmlはかなり似RealWorldていますが、一般化すると...まあ、わかります。
ephemient

4
IO後者は相互作用、並行性、非決定性をサポートしているため、この説明は実際にはHaskellに適用できないことに注意してください。他のポインタについては、この質問に対する私の回答を参照してください。
Conal

2
@Conal GHCは実際にIOこの方法を実装しますが、RealWorld実際には実際の世界を表しません。これは、操作を順序どおりに保つための単なるトークンです(「マジック」は、RealWorldGHC Haskellの唯一の一意性タイプです)
Jeremy List

2
@JeremyList私が理解しているように、GHC IOはこの表現と非標準のコンパイラマジックケントンプソンの有名なCコンパイラウイルスを連想させる)の組み合わせを介して実装されます。他のタイプの場合、真実は通常のHaskellセマンティクスとともにソースコードにあります。
Conal

1
@Clonal私のコメントは、GHCソースコードの関連部分を読んだことによるものです。
ジェレミーリスト

43

Haskellの純粋でない計算がモナドとしてモデル化されている理由について、誰かがいくつかの指針を示すことができますか?

この質問には、広範な誤解が含まれています。不純物とモナドは独立した概念です。不純物はモナドによってモデル化されていません。むしろ、IO命令型計算を表すのようないくつかのデータ型があります。これらのタイプの一部では、それらのインターフェースのごく一部が「モナド」と呼ばれるインターフェースパターンに対応しています。さらに、の意味について一般的に伝えられている話がありますが、純粋で機能的で代表的な説明IOはありません(そして、「罪箱」の目的を考えると、説明がありそうにありませんIO)。その物語は正直に説明することはできません、なぜならWorld -> (a, World)IO aIOIO並行性と非決定性をサポートします。中途半端な世界との相互作用を可能にする決定論的計算の場合、ストーリーはうまくいきません。

詳細については、この回答を参照してください。

編集:質問をもう一度読んで、私の答えは順調に進んでいるとは思いません。命令型計算のモデルは、質問のとおり、モナドであることがよくあります。質問者は、モナドネスが決して命令型計算のモデリングを可能にすることを実際には想定していません。


1
@KennyTM:しかしRealWorld、ドキュメントが言うように、「深く魔法」です。これは、ランタイムシステムが実行していることを表すトークンであり、実際には実世界について何も意味しません。余分なトリックを行わないと、新しいスレッドを作成して「スレッド」を作成することさえできません。素朴なアプローチでは、いつ実行するかについて多くのあいまいさを持つ単一のブロックアクションを作成するだけです。
CAマッキャン2011

4
また、モナド本質的に本質的に必須であると私は主張します。ファンクタが値が埋め込まれた構造を表す場合、モナドインスタンスは、それらの値に基づいて新しいレイヤを構築およびフラット化できることを意味します。したがって、ファンクタの単一のレイヤに割り当てる意味が何であれ、モナドは、1つから次の段階に至る因果関係の厳密な概念を持つ無制限の数のレイヤを作成できることを意味します。特定のインスタンスは本質的に命令型の構造を持たない場合がありますがMonad、一般的には実際にはそうです。
CAマッキャン

3
Monad一般的に」とは、大まかにforall m. Monad m => ...、つまり任意のインスタンスで作業することを意味します。任意のモナドで実行できることは、実行可能なものとほぼ同じですIO:不透明なプリミティブを(関数の引数として、またはライブラリからそれぞれ)受け取り、で何もしないを構築するreturnか、またはを使用して不可逆的な方法で値を変換します(>>=)。任意のモナドでのプログラミングの本質は、取り消し可能なアクションのリストを生成することです:「Xを実行してから、Yを実行してから...」。私にはかなり不可欠に聞こえます!
CAマッキャン

2
いいえ、あなたはまだここで私のポイントを逃しています。もちろん、それらは明確で意味のある構造を持っているので、それらの特定のタイプにはその考え方を使用しません。「任意のモナド」とは、「どれを選ぶかわからない」という意味です。ここでの視点は量指定子の内部からのものであるためm、実存的であると考える方が役立つ場合があります。さらに、私の「解釈」は法律の言い換えです。「do X」ステートメントのリストは、を介して作成された不明な構造のフリーモノイド(>>=)です。そしてモナドの法則は、内部ファンクターの構成に関する単なるモノイドの法則です。
CAマッキャン

3
要するに、すべてのモナドが一緒に説明することの最大の下限は、未来への盲目で意味のない行進です。IOこれは病理学的なケースです。これは、この最小値以外にほとんど何も提供しないためです。特定のケースでは、タイプはより多くの構造を明らかにし、したがって実際の意味を持つ場合があります。しかし、それ以外の場合、法則に基づくモナドの本質的な特性は、表記を明確にするのと同じように正反対IOです。コンストラクタをエクスポートしたり、プリミティブなアクションなどを徹底的に列挙したりしなければ、状況は絶望的です。
CAマッキャン、2011

13

私が理解しているように、Eugenio Moggiと呼ばれる人は、「モナド」と呼ばれていたあいまいな数学的構造がコンピューター言語で副作用をモデル化するために使用でき、ラムダ計算を使用してセマンティクスを指定できることに最初に気付きました。Haskellが開発されたとき、不純な計算がモデル化されるさまざまな方法がありました(詳細については、Simon Peyton Jonesの「ヘアシャツ」の論文を参照)。しかし、Phil Wadlerがモナドを導入したとき、これが答えであることがすぐに明らかになりました。そして残りは歴史です。


3
結構です。モナドは非常に長い間解釈をモデル化できることが知られています(少なくとも「トポイ:論理のカテゴリー分析」以降)。一方、強く型付けされた関数型になるまで、モナドの型を明確に表現することはできませんでした言語が登場し、Moggiは2つと2つを組み合わせました
名詞

1
モナドは、マップのラップとアンラップに関して定義されていれば、理解しやすくなる可能性があります。
aoeu256

9

Haskellの純粋でない計算がモナドとしてモデル化されている理由について、誰かがいくつかの指針を示すことができますか?

まあ、Haskellは純粋だからです。あなたは区別するために、数学的概念を必要とする不純な計算純粋なものタイプレベルとモデルへのプログラムの開発フローをそれぞれインチ

これはIO a、純粋でない計算をモデル化するいくつかのタイプで終わる必要があることを意味します。次にこれらの計算を組み合わせる方法を知っておく必要があります。これらの計算は、順番に適用され>>=、値持ち上げますreturn)が最も明白で基本的なものです。

これらの2つで、モナドはすでに定義されています(考えもしていません)。

さらに、モナドは非常に一般的で強力な抽象化を提供するのでsequenceliftMなどのモナド関数や特別な構文で多くの種類の制御フローを簡単に一般化でき、不純さを特別な場合にはしません。

詳細については、関数型プログラミング一意性タイピング(私が知っている唯一の代替手段)のモナドを参照してください。


6

おっしゃるとおりMonad、非常にシンプルな構造です。答えの半分は次のとおりです。Monad副作用のある関数に与えて、それらを使用できるようにすることができる最も単純な構造です。ではMonad、我々は2つのことを行うことができます。私たちは副作用値(純粋な値を扱うことができますreturn)、そして我々は新しい副作用の値を取得するには副作用の値に副作用関数を適用することができます(>>=)。これらのいずれかを実行する能力を失うことは不自由なことになるので、副作用タイプは「少なくとも」MonadであるMonad必要があり、これまでに必要なすべてを実装するのに十分であることがわかります。

残りの半分は、「起こり得る副作用」に与えることができる最も詳細な構造は何ですか?私たちは確かにすべての可能な副作用の空間をセットとして考えることができます(必要な唯一の演算はメンバーシップです)。2つの副作用を次々に組み合わせることで組み合わせることができます。これにより、異なる副作用(またはおそらく同じ効果)が発生します。最初のものが「シャットダウンコンピュータ」で、2番目が「ファイルの書き込み」の場合、結果はこれらを構成することは、単なる「シャットダウンコンピュータ」です。

では、この操作について何が言えるでしょうか。それは連想的です。つまり、3つの副作用を組み合わせる場合、どの順序で組み合わせるかは関係ありません。行う(ファイルを書き込んでからソケットを読み取る)とコンピューターをシャットダウンする場合は、ファイルを書き込む(ソケットを読み取ってシャットダウンする)と同じです。コンピューター)。しかし、それは可換ではありません:(「ファイルを書き込む」、「ファイルを削除する」)は(「ファイルを削除する」、「ファイルを書き込む」)とは異なる副作用です。そして私たちにはアイデンティティがあります:特別な副作用「副作用なし」は機能します(「副作用なし」、「ファイルの削除」は「ファイルの削除」と同じ副作用です)この時点で、数学者は「グループ!」と考えています。しかし、グループには逆があり、一般に副作用を反転させる方法はありません。"ファイルを削除する" 不可逆です。したがって、残した構造はモノイドの構造です。つまり、副作用のある関数はモナドでなければなりません。

より複雑な構造はありますか?承知しました!可能性のある副作用をファイルシステムベースの効果、ネットワークベースの効果などに分割し、これらの詳細を保持するより複雑な合成ルールを考え出すことができます。しかし、繰り返しになりますMonadが、非常にシンプルでありながら、気になるプロパティのほとんどを表現するのに十分強力です。(特に、結合性と他の公理により、アプリケーションを小さな部分でテストできます。組み合わせたアプリケーションの副作用は、部分の副作用の組み合わせと同じになると確信しています)。


4

これは実際に、I / Oを機能的に考えるのに非常にクリーンな方法です。

ほとんどのプログラミング言語では、入出力操作を行います。Haskellでは、しないの書き込みコードを想像して行う操作を、しかし、あなたがやりたい操作のリストを生成します。

モナドはまさにそれのためのかなりの構文です。

なぜモナドが他のものではないのかを知りたいのであれば、答えは、Haskellを作っているときに人々が考えるI / Oを表現するための最良の機能的な方法だと思います。


3

私の知る限り、理由は型システムに副作用チェックを含めることができるようにするためです。詳細を知りたい場合は、これらのSE-Radioエピソードを聞いてください。エピソード108:関数型プログラミングのSimon Peyton JonesおよびHaskellエピソード72:LINQのErik Meijer


2

上記は理論的背景を持つ非常に良い詳細な回答です。しかし、私はIOモナドについて私の見解を述べたいと思います。私はhaskellプログラマーを経験していませんので、それは非常に素朴であるか間違っているかもしれません。しかし、私はある程度IOモナドを扱うのを手伝いました(他のモナドとは関係がないことに注意してください)。

最初に言いたいのは、その「現実の世界」の例は、その(現実の世界)以前の状態にアクセスできないため、私にはあまり明確ではありません。モナド計算とはまったく関係ないかもしれませんが、参照透過性という意味で望ましいものです。これは、一般にHaskellコードに存在します。

したがって、私たちは言語(ハスケル)を純粋にしたいと考えています。ただし、入出力操作がないとプログラムが役に立たないため、入出力操作が必要です。そして、それらの操作は、その性質上、純粋なものにすることはできません。したがって、これに対処する唯一の方法は、不純な操作を残りのコードから分離することです。

ここにモナドが来る。実際には、同様の必要なプロパティを持つ他の構成が存在できないかどうかはわかりませんが、ポイントはモナドにこれらのプロパティがあるため、使用できる(そしてそれが正常に使用される)ことです。主な特性は、それから逃れることができないことです。モナドインターフェースには、値の周囲のモナドを取り除く操作はありません。他の(IOではない)モナドはそのような操作を提供し、パターンマッチングを許可します(たぶん)。しかし、それらの操作はモナドインターフェースにはありません。別の必要なプロパティは、操作をチェーンする機能です。

型システムの観点から何が必要かを考えると、コンストラクター付きの型が必要であり、どんな値にもラップすることができます。コンストラクターはエスケープ(パターンマッチング)を禁止しているため、プライベートである必要があります。しかし、このコンストラクターに値を入れるための関数が必要です(ここでは戻りが思い浮かびます)。そして、オペレーションをチェーンする方法が必要です。しばらく考えてみると、連鎖演算には>> =のように型が必要であるということに気づくでしょう。それで、モナドに非常によく似たものになります。この構造で矛盾する可能性のある状況を分析すると、モナドの公理になります。

開発された構成には、不純物との共通点がないことに注意してください。これには、不純な操作、つまり、エスケープなし、チェーン、および取得方法に対処できるようにしたいプロパティのみがあります。

現在、不純な操作のセットが、この選択されたモナドIO内の言語によって事前定義されています。これらの操作を組み合わせて、新しい純粋でない操作を作成できます。そして、それらすべての操作には、そのタイプのIOが必要です。ただし、一部の関数のタイプにIOが存在しても、この関数が不純にならないことに注意してください。しかし、私が理解しているように、最初は純粋な関数と不純な関数を分離するというのが私たちの考えだったので、タイプにIOを含む純粋な関数を書くことは悪い考えです。

最後に、モナドは純粋でない操作を純粋な操作に変えないことを言いたいです。それらを効果的に分離することができます。(繰り返しますが、それは私の理解にすぎません)


1
チェックエフェクトを入力することでプログラムのタイプチェックを支援し、モナドを作成して独自のDSLを定義し、関数が実行できるエフェクトを制限してコンパイラがシーケンスエラーをチェックできるようにします。
aoeu256

aoeu256からのこのコメントは、これまでのすべての説明に欠けている「理由」です。(すなわち:モナドは人間のために、しかし、コンパイラではありません)
ジョアン・オテロ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.