web-dev-qa-db-ja.com

なぜモナドが必要なのですか?

私の控え目な意見では、有名な質問 "モナドとは何ですか?" 、特に投票されているものに対する答えは、なしのモナドとは何かを説明するようにしてくださいモナドが本当に必要な理由を明確に説明しています。彼らは問題の解決策として説明することができますか?

343
cibercitizen1

なぜモナドが必要なのですか?

  1. プログラムしたい関数のみを使用。 (結局「関数型プログラミング(FP)」)。
  2. 次に、最初の大きな問題があります。これはプログラムです:

    f(x) = 2 * x

    g(x,y) = x / y

    どうすれば最初に実行するものと言うことができますか?関数の順序付けられたシーケンスを形成するにはどうすればよいですか(つまりa program functions

    解決策:compose functions。最初にgを、次にfが必要な場合は、単にf(g(x,y))と記述します。このように、「プログラム」も関数です:main = f(g(x,y))。 OK、しかし...

  3. その他の問題:一部の関数失敗する可能性がある(つまり、g(2,0)、0で除算)。 FPに「例外」なしがあります(例外は関数ではありません)。どのように解決しますか?

    解決策:関数が2種類のものを返すことを許可するg : Real,Real -> Real(2つの実数から実数への関数)を使用する代わりに、g : Real,Real -> Real | Nothing(2つの実数から(実数またはなし)への関数)を許可しましょう。

  4. ただし、関数は(より簡単にするために)1つのことのみを返す必要があります。

    解決策:返される新しいタイプのデータ、「boxing type」を作成します。したがって、g : Real,Real -> Maybe Realを使用できます。 OK、しかし...

  5. f(g(x,y))はどうなりますか? fMaybe Realを使用する準備ができていません。また、gに接続してMaybe Realを使用できるすべての関数を変更する必要はありません。

    解決策:「接続」/「構成」/「リンク」機能に特別な機能がありますにしましょう。このようにして、舞台裏で、1つの関数の出力を調整して、次の関数にフィードすることができます。

    この場合:g >>= fgfに接続/構成)。 >>=gの出力を取得して検査し、それがNothingである場合は、fを呼び出してNothingを返さないでください。または、逆に、ボックス化されたRealを抽出し、それをfにフィードします。 (このアルゴリズムはMaybe型の>>=の単なる実装です)。また、>>=一度だけ "ボクシングタイプ"(異なるボックス、異なる適応アルゴリズム)ごとに記述する必要があることに注意してください。

  6. この同じパターンを使用して解決できる他の多くの問題が発生します。1.「ボックス」を使用して異なる意味/値をコード化/保存し、それらの「ボックス化された値」を返すgなどの関数を使用します。 2. gの出力をfの入力に接続しやすくするために、コンポーザー/リンカーg >>= fを用意します。したがって、fを変更する必要はありません。

  7. この手法を使用して解決できる顕著な問題は次のとおりです。

    • 関数のシーケンス内のすべての関数(「プログラム」)が共有できるグローバル状態:ソリューションStateMonad

    • 「不純な関数」は好きではありません。 different same 入力に対して出力する関数です。したがって、これらの関数をマークして、タグ付き/ボックス化された値を返すようにします:IO monad。

完全な幸福!

551
cibercitizen1

答えは、もちろん、「私たちはしない」です。すべての抽象化と同様に、それは必要ではありません。

Haskellはモナドの抽象化を必要としません。純粋な言語でIOを実行する必要はありません。 IO型はそれだけで大丈夫です。既存のdoブロックのモナディックデスガリングは、GHC.Baseモジュールで定義されているように、bindIOreturnIO、およびfailIOへのデスガードに置き換えられます。 (これは文書化されたハッカーのモジュールではないので、私は そのソース を文書化するために指摘しなければならないでしょう。)だから、モナドは必要ありません。抽象化です。

それが必要でなければ、なぜそれが存在するのでしょうか?多くの計算パターンがモナド構造を形成することがわかったからです。構造を抽象化すると、その構造のすべてのインスタンスにわたって機能するコードを書くことができます。より簡潔に言うと、コードの再利用です。

関数型言語では、コードを再利用するための最も強力なツールは関数の合成です。古き(.) :: (b -> c) -> (a -> b) -> (a -> c)演算子は非常に強力です。それは、小さな関数を書いて、最小限の構文上または意味上のオーバーヘッドでそれらを一緒に接着することを容易にします。

しかし、型がまったくうまくいかない場合があります。 foo :: (b -> Maybe c)bar :: (a -> Maybe b)があるときあなたは何をしますか? foo . barはタイプチェックされません。なぜなら、bMaybe bは同じ型ではないからです。

しかし……ほぼ正しいです。あなたはちょっとした余裕がほしいのです。あなたは、Maybe bを基本的にbであるかのように扱うことができるようにしたいです。ただし、それらを同じタイプとして扱うだけでは不十分です。それは、Tony Hoareが有名に言った 10億ドルの間違い と同じことです。そのため、それらを同じ型として扱うことができない場合は、(.)が提供する構成メカニズムを拡張する方法を見つけることができます。

その場合、(.)の根底にある理論を実際に調べることが重要です。幸いなことに、誰かがすでに私たちのためにこれをやっています。 (.)idの組み合わせは、 カテゴリ として知られる数学的な構成要素を形成することがわかりました。しかし、カテゴリを形成する他の方法があります。たとえば、Kleisliカテゴリでは、構成されているオブジェクトを少し増やすことができます。 MaybeのKleisliカテゴリは、(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)id :: a -> Maybe aで構成されます。つまり、カテゴリ内のオブジェクトは(->)Maybeで補強するため、(a -> b)(a -> Maybe b)になります。

そして突然、合成の力を従来の(.)操作では機能しないものにまで拡張しました。これが新しい抽象化力の源です。 Kleisliカテゴリは単なるMaybe以上の型で機能します。それらは、カテゴリー法を遵守しながら、適切なカテゴリーを組み立てることができるあらゆるタイプで動作します。

  1. 左の識別情報:id . f = f
  2. 正しいID:f . id = f
  3. 結合性:f . (g . h) = (f . g) . h

あなたのタイプがこれらの3つの法則に従っていることを証明できる限り、あなたはそれをKleisliカテゴリに変えることができます。そしてそれについて大したことは何ですか?実のところ、モナドはKleisliのカテゴリとまったく同じものです。 Monadreturnは、Kleisliのidと同じです。 Monad(>>=)はKleisliの(.)と同一ではありませんが、もう一方に関して非常に簡単に書くことができます。 (>>=)(.)の違いをまたいで変換すると、カテゴリー法はモナド法と同じになります。

それでは、なぜこの面倒なことをすべて経験するのでしょうか。なぜ言語にMonadという抽象概念があるのでしょうか。上記で触れたように、それはコードの再利用を可能にします。 2つの異なる次元に沿ってコードを再利用することさえ可能にします。

コードの再利用の最初の次元は抽象化の存在から直接来ます。抽象化のすべてのインスタンスにわたって機能するコードを書くことができます。すべての monad-loops パッケージが、Monadのどのインスタンスでも動作するループで構成されています。

2番目の次元は間接的ですが、それは構成の存在から生じます。合成が簡単なときは、小さくて再利用可能なまとまりでコードを書くのが自然です。これは、関数の(.)演算子を使用して、小さくて再利用可能な関数を書くのを促進するのと同じ方法です。

では、なぜ抽象化が存在するのでしょうか。それは、コードのより多くの合成を可能にするツールであることが証明されているため、再利用可能なコードを作成し、より再利用可能なコードの作成を促進するためです。コードの再利用はプログラミングの聖杯の1つです。モナドの抽象化は、それが私たちをその聖杯の方へ少し動かすために存在します。

210
Carl

Benjamin Pierceは TAPL で述べています

型システムは、プログラム内の用語の実行時の動作に対する一種の静的近似を計算すると見なすことができます。

強力な型システムを備えた言語は、型の悪い言語よりも厳密に表現力が高いのです。モナドについても同じように考えることができます。

@Carlと sigfpe を参照すると、モナド、型クラス、その他の抽象的なものに頼ることなく、必要なすべての操作をデータ型に装備できます。しかしモナドを使うと、再利用可能なコードを書くだけでなく、すべての冗長な詳細を抽象化することもできます。

例として、リストをフィルタリングしたいとしましょう。最も簡単な方法はfilter関数を使うことです:filter (> 3) [1..10][4,5,6,7,8,9,10]と同じです。

filterのもう少し複雑なバージョンは、アキュムレータも左から右に渡します。

swap (x, y) = (y, x)
(.*) = (.) . (.)

filterAccum :: (a -> b -> (Bool, a)) -> a -> [b] -> [b]
filterAccum f a xs = [x | (x, True) <- Zip xs $ snd $ mapAccumL (swap .* f) a xs]

i <= 10, sum [1..i] > 4, sum [1..i] < 25のようにすべてのiを取得するには、

filterAccum (\a x -> let a' = a + x in (a' > 4 && a' < 25, a')) 0 [1..10]

これは[3,4,5,6]と同じです。

あるいは、nubに関して、リストから重複する要素を削除するfilterAccum関数を再定義することができます。

nub' = filterAccum (\a x -> (x `notElem` a, x:a)) []

nub' [1,2,4,5,4,3,1,8,9,4][1,2,4,5,3,8,9]と同じです。リストはここにアキュムレータとして渡されます。リストモナドを残すことが可能で、計算全体が純粋なままであるため、コードは機能します(notElemは実際には>>=を使用しませんが、可能です)。ただし、IOモナドを安全に終了することはできません(つまり、IOアクションを実行して純粋な値を返すことはできません。値は常にIOで囲まれます)。 _モナド)もう1つの例は可変配列です。STモナドから抜け出した後、可変配列が存在すると、もう一定の時間内にその配列を更新することはできません。そのため、Control.Monadモジュールからのモナディックフィルタリングが必要です。

filterM          :: (Monad m) => (a -> m Bool) -> [a] -> m [a]
filterM _ []     =  return []
filterM p (x:xs) =  do
   flg <- p x
   ys  <- filterM p xs
   return (if flg then x:ys else ys)

filterMは、リストのすべての要素に対してモナディックアクションを実行し、要素を生成します。この要素に対して、モナディックアクションはTrueを返します。

配列を使ったフィルタリングの例:

nub' xs = runST $ do
        arr <- newArray (1, 9) True :: ST s (STUArray s Int Bool)
        let p i = readArray arr i <* writeArray arr i False
        filterM p xs

main = print $ nub' [1,2,4,5,4,3,1,8,9,4]

期待どおりに[1,2,4,5,3,8,9]を出力します。

また、IOモナドを持つバージョンでは、どの要素を返すかを尋ねます。

main = filterM p [1,2,4,5] >>= print where
    p i = putStrLn ("return " ++ show i ++ "?") *> readLn

例えば。

return 1? -- output
True      -- input
return 2?
False
return 4?
False
return 5?
True
[1,5]     -- output

そして最後の例として、filterAccumfilterMの観点から定義することができます。

filterAccum f a xs = evalState (filterM (state . flip f) xs) a

StateTモナドでは、それはボンネットの下で使用され、単なる普通のデータ型です。

この例は、モナドが(@Carlが説明するようにモナドの合成可能性のために)計算コンテキストを抽象化し、きれいな再使用可能なコードを書くことを可能にするだけでなく.

24
user3237465

IOは特に優れたモナドと見なされるべきではないと思いますが、初心者にとっては驚くべきモナドの1つであるため、説明に使用します。

Haskell用のIOシステムをナイーブに構築する

純粋に機能的な言語(そして実際にはHaskellが始めた言語)の最も簡単なIOシステムは次のとおりです。

main₀ :: String -> String
main₀ _ = "Hello World"

怠zyで、その単純な署名は、実際にインタラクティブな端末プログラムを構築するのに十分です– veryただし、制限されています。最もイライラするのは、テキストしか出力できないことです。さらにエキサイティングな出力の可能性を追加した場合はどうなりますか?

data Output = TxtOutput String
            | Beep Frequency

main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
          -- , Beep 440  -- for debugging
          ]

かわいいですが、もちろん、より現実的な「代替出力」はファイルへの書き込みです。しかし、それからread fromファイルへの何らかの方法も必要です。チャンスはありますか?

main₁プログラムを使用して、単純にファイルをプロセスにパイプする(オペレーティングシステムの機能を使用)を取得すると、基本的にファイル読み取りが実装されます。 Haskell言語内からそのファイル読み取りをトリガーできる場合...

readFile :: Filepath -> (String -> [Output]) -> [Output]

これは、「インタラクティブプログラム」String->[Output]を使用し、ファイルから取得した文字列をフィードし、指定されたものを単に実行する非インタラクティブプログラムを生成します。

ここには1つの問題があります。実際にはwhenというファイルの読み取りという概念がありません。 [Output]リストは、確かにoutputsに素敵な順序を与えますが、inputsがいつ行われるかについての順序は得られません。

解決策:入力イベントも、実行することのリストの項目にします。

data IO₀ = TxtOut String
         | TxtIn (String -> [Output])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [Output])
         | Beep Double

main₂ :: String -> [IO₀]
main₂ _ = [ FileRead "/dev/null" $ \_ ->
             [TxtOutput "Hello World"]
          ]

OK、今、あなたは不均衡を見つけるかもしれません:あなたはファイルを読んで、それに依存する出力をすることができます、しかし、あなたは、例えば別のファイルも読み取ります。明らかな解決策:入力イベントの結果も、IOだけでなくOutput型の結果にします。確かに単純なテキスト出力が含まれていますが、追加のファイルなどを読み取ることもできます。

data IO₁ = TxtOut String
         | TxtIn (String -> [IO₁])
         | FileWrite FilePath String
         | FileRead FilePath (String -> [IO₁])
         | Beep Double

main₃ :: String -> [IO₁]
main₃ _ = [ TxtIn $ \_ ->
             [TxtOut "Hello World"]
          ]

これにより、実際にはプログラムで必要なファイル操作を表現できるようになります(ただし、おそらくパフォーマンスは良くありません)が、やや複雑すぎます。

  • main₃は、アクション全体リストを生成します。特殊なケースとしてこれを持っている署名:: IO₁を単に使用しないのはなぜですか?

  • リストは、プログラムフローの信頼できる概要を実際にはもう提供していません。ほとんどの後続の計算は、入力操作の結果としてのみ「アナウンス」されます。したがって、リスト構造を捨てて、各出力操作を単純に「してから」実行することもできます。

data IO₂ = TxtOut String IO₂
         | TxtIn (String -> IO₂)
         | Terminate

main₄ :: IO₂
main₄ = TxtIn $ \_ ->
         TxtOut "Hello World"
          Terminate

悪くない!

それでは、これらすべてがモナドと何の関係があるのでしょうか?

実際には、単純なコンストラクターを使用してすべてのプログラムを定義することは望ましくありません。そのような基本的なコンストラクターをいくつか用意する必要がありますが、ほとんどの高レベルのものについては、ニースの高レベルシグネチャを使用して関数を記述します。これらのほとんどは非常によく似ていることがわかります。ある種の意味のある型指定された値を受け入れ、結果としてIOアクションを生成します。

getTime :: (UTCTime -> IO₂) -> IO₂
randomRIO :: Random r => (r,r) -> (r -> IO₂) -> IO₂
findFile :: RegEx -> (Maybe FilePath -> IO₂) -> IO₂

ここには明らかにパターンがあります。

type IO₃ a = (a -> IO₂) -> IO₂    -- If this reminds you of continuation-passing
                                  -- style, you're right.

getTime :: IO₃ UTCTime
randomRIO :: Random r => (r,r) -> IO₃ r
findFile :: RegEx -> IO₃ (Maybe FilePath)

今では見慣れているように見えますが、私たちはまだ内部で薄く偽装された単純な関数を扱っているだけであり、リスクがあります。プログラム全体の制御フローは、途中で1つの不適切な動作によって簡単に中断されます)。その要件を明確にすることをお勧めします。まあ、それらはモナドの法則であることがわかりますが、標準のバインド/ジョイン演算子なしでそれらを実際に定式化できるかどうかはわかりません。

とにかく、適切なモナドインスタンスを持つIOの定式化に到達しました。

data IO₄ a = TxtOut String (IO₄ a)
           | TxtIn (String -> IO₄ a)
           | TerminateWith a

txtOut :: String -> IO₄ ()
txtOut s = TxtOut s $ TerminateWith ()

txtIn :: IO₄ String
txtIn = TxtIn $ TerminateWith

instance Functor IO₄ where
  fmap f (TerminateWith a) = TerminateWith $ f a
  fmap f (TxtIn g) = TxtIn $ fmap f . g
  fmap f (TxtOut s c) = TxtOut s $ fmap f c

instance Applicative IO₄ where
  pure = TerminateWith
  (<*>) = ap

instance Monad IO₄ where
  TerminateWith x >>= f = f x
  TxtOut s c >>= f = TxtOut s $ c >>= f
  TxtIn g >>= f = TxtIn $ (>>=f) . g

明らかにこれはIOの効率的な実装ではありませんが、原則としては使用可能です。

19
leftaroundabout

モナドは基本的に機能を連鎖的にまとめる働きをします。期間。

現在、それらが構成する方法は既存のモナド間で異なっています、その結果、異なった振る舞いをもたらします(例えば、状態モナドの可変状態をシミュレートするために)。

モナドについての混乱は、非常に一般的な、つまり関数を構成するためのメカニズムであり、さまざまなことに使用できるため、モナドは単に「関数を構成することに関する」状態であるということです。 ".

さて、モナドについて興味深いことは、合成の結果は常にタイプ "M a"、つまり "M"でタグ付けされたエンベロープ内の値であるということです。この機能は、純粋なコードと不純なコードを明確に区別するために実装すると非常に便利です。すべての不純なアクションを "IO a"型の関数として宣言し、IOモナドを定義するときに関数を提供しない「IO a」の内側から「a」値を取り出す。その結果、純粋であり続ける一方でそのような値を取る方法がないので、どの関数も純粋であり得ず、同時に「IO a」から値を取り出すことができません(使用するためには関数は「IO」モナドの内側になければなりません)そのような値)。 (注:まあ、完璧なものは何もないので、 "unsafePerformIO:IO a - > a"を使用して "IO straitjacket"を壊すことができます)非常に控えめに、そしてあなたが本当に副作用を伴う少しの不純なコードも導入しないことを知っている時。

3
mljrg

モナド は、繰り返し発生する問題のクラスを解決するための便利なフレームワークです。まず、モナドは functors でなければなりません(すなわち、要素(またはその型)を見ずにマッピングをサポートしなければなりません)、また binding (またはchaining)操作とモナド値を作成する方法要素型(return)から。最後に、bindreturnは、モナド則とも呼ばれる2つの方程式(左辺と右辺の恒等式)を満たす必要があります。 (あるいは、束縛の代わりにflattening operationを持つようにモナドを定義することもできます。)

list monad は、一般的に非決定論を扱うために使われます。 bind操作はリストの要素を1つ選択し(直観的にそれらすべてを parallel worlds で)、プログラマがそれらを使って計算できるようにしてから、すべての世界の結果を単一のリストに結合します。 、ネストしたリスト) Haskellのモナディックフレームワークで順列関数を定義する方法は次のとおりです。

perm [e] = [[e]]
perm l = do (leader, index) <- Zip l [0 :: Int ..]
            let shortened = take index l ++ drop (index + 1) l
            trailer <- perm shortened
            return (leader : trailer)

これが repl sessionの例です。

*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]

リストモナドは決して計算に影響を与えるものではありません。モナドである数学的構造(すなわち、上述のインタフェースおよび法則に準拠する)は、副作用を意味するものではないが、副作用現象はしばしばモナドの枠組みにうまく適合する。

3
heisenbug

型コンストラクタ その型族 の値を返す関数がある場合は、モナドが必要です。結局、あなたは これらの種類の機能を一緒に結合したいです 。これらが答えるべき3つの重要な要素ですなぜ

詳しく説明しましょう。あなたはIntStringReal、そして型Int -> StringString -> Realの関数を持っています。 Int -> Realで終わるように、これらの関数を簡単に組み合わせることができます。人生は素晴らしい。

それから、ある日、 newファミリーの型 を作成する必要があります。値を返さない(Maybe)、エラーを返す(Either)、複数の結果(List)などの可能性を考慮する必要があるためです。

Maybeは型コンストラクタです。 Intのような型を取り、新しい型Maybe Intを返します。覚えておくべき最初のこと、 型コンストラクタ、モナドなし。

もちろん、 あなたは自分のコードの中であなたの型構築子を使いたい そしてそしてすぐにあなたはInt -> Maybe StringString -> Maybe Floatのような関数で終わる。今、あなたは簡単にあなたの機能を組み合わせることはできません。人生はもう良くありません。

そしてここにモナドが救助に来るときです。それらはあなたが再びその種の機能を組み合わせることを可能にします。あなただけの構成を変更する必要がありますfor > == .

2
jdinunzio