web-dev-qa-db-ja.com

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

Haskellの不純な計算がモナドとしてモデル化される理由について、誰かがポインタを与えることができますか?

モナドは4つの操作を備えた単なるインターフェイスであるということです。そのため、モナドの副作用をモデル化する理由は何ですか?

164
bodacydo

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

不純な機能のために

_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は再び純粋になります。パラメータ化されたデータ型IO a = RealWorld -> (a, RealWorld)を定義するため、RealWorldを何度も入力する必要はありません。

_f :: Int -> IO Int
_

プログラマにとって、RealWorldを直接操作するのは非常に危険です。特に、プログラマがRealWorld型の値を取得した場合、基本的に不可能なcopy itを試みる可能性があります。 (たとえば、ファイルシステム全体をコピーしようとすることを考えてください。どこに配置しますか?)したがって、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. 2つの不純な関数を連結する演算子_(~~~) :: IO b -> (b -> IO c) -> IO c_を定義しました
  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) a worldX = let (resF, worldY) = impuify a worldX 
                              in f resF worldY
                            = let (resF, worldY) = (a, worldX)     
                              in f resF worldY
                            = f a worldX
_

(3)f >>= (\x -> g x >>= h) = (f >>= g) >>= h

運動。)

286
kennytm

Haskellでの不純な計算がモナドとしてモデル化されている理由について、誰かがポインタを与えることができますか?

この質問には、広範な誤解が含まれています。不純物とモナドは独立した概念です。不純物は、Monadによってモデル化されたnotです。むしろ、命令型計算を表すIOなどのいくつかのデータ型があります。また、これらのタイプの一部では、インターフェイスのごく一部が「Monad」と呼ばれるインターフェイスパターンに対応しています。さらに、IOの既知の純粋な/機能的/表示的な説明はありません( "sin bin"IOの目的を考慮すると、説明はありません) 、World -> (a, World)IO aIOは並行性と非決定性をサポートしているため、そのストーリーはIOを正確に説明できません。ストーリーは、計算中期の世界との相互作用を可能にする決定論的な計算の場合にも機能しません。

詳細については、 this answer を参照してください。

Edit:質問を読み直したとき、私の答えは順調に進んでいないと思います。質問が言ったように、命令型計算のモデルはしばしばモナドになります。質問者は、モナドネスが何らかの方法で命令型計算のモデリングを可能にすることを実際に想定していないかもしれません。

43
Conal

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

13
Paul Johnson

Haskellでの不純な計算がモナドとしてモデル化されている理由について、誰かがポインタを与えることができますか?

Haskellはpureであるためです。 npure computingspure ones ontype-levelを区別し、モデルを作成するにはプログラムフローそれぞれ。

これは、何らかのタイプIO a不純な計算をモデル化します。次に、combiningの方法を知る必要があります。これらの計算は順番に適用>>=)および値を持ち上げるreturn)は、最も明白で基本的なものです。

これら2つで、あなたはすでにモナドを定義しました(それさえ考えずに);)

さらに、モナドは非常に一般的で強力な抽象化を提供するため、sequenceliftMなどのモナド関数または多くの種類の制御構文で多くの種類の制御フローを便利に一般化できます。そのような特別な場合ではなく、不純。

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

8
Dario

あなたが言うように、Monadは非常に単純な構造です。答えの半分は次のとおりです。Monadは、副作用のある関数に与えて使用できる最も単純な構造です。 Monadを使用すると、2つのことができます。純粋な値を副作用値(return)として扱うことができ、副作用値に副作用関数を適用できます。新しい副作用値(>>=)。これらのいずれかを実行する能力を失うと、障害が発生するため、副作用の型は「少なくとも」Monadである必要があり、Monadですべてを実装することができます。 veはこれまで必要でした。

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

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

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

6
lmm

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

ほとんどのプログラミング言語では、入力/出力操作を行います。 Haskellでは、do操作ではなく、実行したい操作のリストを生成するコードを書くことを想像してください。

モナドはまさにそのための単なるきれいな構文です。

モナドが他の何かとは対照的な理由を知りたいなら、答えは、人々がHaskellを作っているときに考えることができるI/Oを表現するための最も機能的な方法だと思います。

4
Noah Lavine

私の知る限り、その理由は、型システムに副作用チェックを含めることができるようにするためです。もっと知りたいなら、それらを聞いてください SE-Radio エピソード:エピソード108:関数型プログラミングのサイモンペイトンジョーンズとHaskellエピソード72:LINQのエリックメイジャー

3

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

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

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

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

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

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

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

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

2
Dmitrii Semikin