私の控えめな意見では、「モナドとは何ですか?」という有名な質問に対する答えです。、特に最も投票されたものは、モナドが本当に必要な理由を明確に説明せずに、モナドとは何かを説明しようとします。問題の解決策として説明できますか?
私の控えめな意見では、「モナドとは何ですか?」という有名な質問に対する答えです。、特に最も投票されたものは、モナドが本当に必要な理由を明確に説明せずに、モナドとは何かを説明しようとします。問題の解決策として説明できますか?
回答:
次に、最初の大きな問題があります。これはプログラムです:
f(x) = 2 * x
g(x,y) = x / y
最初に何を実行するかをどのように言うことができ ますか?関数だけを使用して、順序付けられた一連の関数(つまりプログラム)をどのように形成できますか?
解決策:関数の作成。最初に必要な場合g
はf
、次のように書いてくださいf(g(x,y))
。このように、「プログラム」も関数ですmain = f(g(x,y))
。OK、でも...
その他の問題:一部の関数は失敗する可能性があります(つまりg(2,0)
、0で除算)。私たちは持っていないない「例外」 FP内を(例外は関数ではありません)。どうすれば解決できますか?
解決策:関数が2つの種類のものを返すことを許可しましょう:g : Real,Real -> Real
(2つの実数から1つの実数への関数)を持つ代わりに、(g : Real,Real -> Real | Nothing
2つの実数から(実数または何もない)への関数)を許可しましょう。
しかし、関数は(より簡単に)1つのものだけを返す必要があります。
解決策:返される新しいタイプのデータを作成してみましょう。「ボクシングタイプ」は、実在するものか、まったく存在しないものを囲みます。したがって、私たちはを持つことができますg : Real,Real -> Maybe Real
。OK、でも...
今はどうなりf(g(x,y))
ますか?f
を使用する準備ができていませんMaybe Real
。そして、私たちはと接続することができ、すべての機能を変更したくないg
消費するようにMaybe Real
。
解決策:関数を「接続」/「作成」/「リンク」する特別な関数を用意しましょう。このようにして、背後で1つの関数の出力を調整して、次の関数にフィードできます。
私たちの場合: g >>= f
(connect / compose g
to f
)。我々はしたい>>=
得るためにg
、それがされた場合には、それを検査し、S」の出力をNothing
単に呼び出すことはありませんf
リターンとNothing
。または逆に、箱に入っているものを取り出し、それと一緒にReal
餌f
をやります。(このアルゴリズムは>>=
、Maybe
型のの実装にすぎません)。また、「ボックス化タイプ」(異なるボックス、異なる適応アルゴリズム)ごとに1 回だけ>>=
記述する必要があることに注意してください。
同じパターンを使用して解決できる他の多くの問題が発生します。1.「ボックス」を使用してさまざまな意味/値をコード化/保存し、g
それらの「ボックス化された値」を返すような関数を使用します。2. の出力をの入力にg >>= f
接続するのに役立つコンポーザー/リンカーがあるため、何も変更する必要はありません。g
f
f
この手法を使用して解決できる注目すべき問題は次のとおりです。
関数のシーケンス内のすべての関数(「プログラム」)が共有できるグローバルな状態:ソリューションStateMonad
。
「純粋でない関数」:同じ入力に対して異なる出力を生成する関数は好きではありません。したがって、これらの関数にマークを付けて、タグ付き/ボックス化された値を返すようにします:モナド。IO
完全な幸せ!
IO
モナドがリストのもう1つの問題にすぎないIO
(ポイント7)ことは答えで明らかだと思います。一方IO
、最後と最後にしか表示されないので、「ほとんどの場合、IOについて話している...」とは理解しないでください。
Either
)。答えのほとんどは「なぜファンクタが必要なのか」についてです。
g >>= f
接続するのに役立つコンポーザー/リンカーがあるので、何も変更する必要はありません。」g
f
f
これはまったく正しくありません。前、中f(g(x,y))
、f
何かを作り出すことができます。可能性がありますf:: Real -> String
。「モナディック構成」では、生成するように変更する必要があります。変更しないとMaybe String
、タイプが適合しません。また、>>=
それ自体が合わない !! それ>=>
はこの構成を行うのではなく、>>=
です。カールの答えの下でdfeuerとの議論を参照してください。
答えは、もちろん「私たちはしません」です。すべての抽象化と同様に、それは必要ではありません。
Haskellはモナドの抽象化を必要としません。純粋な言語でIOを実行する必要はありません。IO
タイプは、それ自体でそのちょうど良いの世話をします。既存のモナド脱糖do
ブロックは、脱糖に置き換えることができるbindIO
、returnIO
とfailIO
のように定義されているGHC.Base
モジュール。(ハッキングに関する文書化されたモジュールではないため、文書化のためにそのソースをポイントする必要があります。)したがって、モナド抽象化の必要はありません。
それで、それが必要ない場合、なぜそれが存在するのですか?なぜなら、多くの計算パターンがモナド構造を形成していることがわかったからです。構造の抽象化により、その構造のすべてのインスタンスで機能するコードを記述できます。より簡潔に言えば、コードの再利用です。
関数型言語では、コードを再利用するための最も強力なツールは関数の合成です。古き良き(.) :: (b -> c) -> (a -> b) -> (a -> c)
オペレーターは非常に強力です。小さな関数を簡単に作成し、最小限の構文または意味上のオーバーヘッドでそれらを結合することが容易になります。
しかし、型が正しく機能しない場合があります。あなたが持っているとき、あなたは何をしますかfoo :: (b -> Maybe c)
とbar :: (a -> Maybe b)
?とは同じ型ではないfoo . bar
ためb
、型チェックを行いませんMaybe b
。
しかし...それはほぼ正しい。ちょっとした余裕が欲しいだけです。Maybe b
基本的にあたかも扱えるようにしたいb
。ただし、これらを同じタイプとして完全に扱うことはお勧めできません。これは、Tony Hoareが有名な10億ドルの間違いと言っていたヌルポインターとほぼ同じです。したがって、それらを同じタイプとして処理できない場合は、合成メカニズムが(.)
提供する拡張方法を見つけることができるでしょう。
その場合、の根底にある理論を実際に検討することが重要(.)
です。幸いなことに、誰かがすでにこれを行ってくれました。カテゴリと呼ばれる数学的構造を組み合わせ(.)
てid
形成することがわかりました。しかし、カテゴリーを形成する他の方法があります。たとえば、Kleisliカテゴリーでは、構成されているオブジェクトを少し増やすことができます。のクライスリカテゴリMaybe
は、(.) :: (b -> Maybe c) -> (a -> Maybe b) -> (a -> Maybe c)
およびで構成されますid :: a -> Maybe a
。つまり、カテゴリオーグメント内のオブジェクトである(->)
とMaybe
、そう(a -> b)
なりました(a -> Maybe b)
。
そして突然、従来の(.)
操作では機能しないものに合成の力を拡大しました。これは新しい抽象化力の源です。Kleisliカテゴリーは、単なるタイプよりも多くのタイプで機能しMaybe
ます。それらは、カテゴリー法に従って、適切なカテゴリーを組み立てることができるすべてのタイプで機能します。
id . f
=f
f . id
=f
f . (g . h)
=(f . g) . h
型がこれらの3つの法則に従っていることを証明できる限り、それをクライスリカテゴリに変えることができます。そして、それの何が大事なのですか?さて、モナドはクライスリカテゴリとまったく同じであることがわかります。Monad
さんはreturn
Kleisliと同じですid
。Monad
さんは(>>=)
Kleisliと同じではない(.)
が、それは他の面でそれぞれを書くことは非常に簡単であることが判明しました。そして、カテゴリの法律では、違いを越え、それらを変換する際に、モナドの法則と同じである(>>=)
と(.)
。
では、なぜこのような面倒な作業をすべて行うのですか Monad
言語に抽象化があるのはなぜですか?上記で触れたように、コードを再利用できます。2つの異なる次元でコードを再利用することもできます。
コードの再利用の最初の側面は、抽象化の存在から直接もたらされます。抽象化のすべてのインスタンスで機能するコードを記述できます。の任意のインスタンスで動作するループで構成されるモナドループパッケージ全体がありますMonad
。
二次元は間接的ですが、それは構成の存在から来ています。構成が簡単な場合は、コードを小さく再利用可能なチャンクで書くのが自然です。これは、(.)
関数の演算子を使用することで、小さくて再利用可能な関数の作成を促進するのと同じです。
では、なぜ抽象化が存在するのでしょうか?これは、コードをさらに構成できるツールであることが証明されているため、再利用可能なコードが作成され、より再利用可能なコードの作成が促進されます。コードの再利用は、プログラミングの神聖なグライルの1つです。モナドの抽象化が存在するのは、それが私たちを少しその聖杯に向かって動かすからです。
newtype Kleisli m a b = Kleisli (a -> m b)
。クライスリカテゴリは、カテゴリ型の戻り値の型(b
この場合)が型コンストラクタの引数である関数m
です。iff Kleisli m
がカテゴリを形成する場合m
、モナドです。
Kleisli m
そのオブジェクトから矢印というHaskellの種類と、このようなあるカテゴリーを形成すると思われるa
のは、b
関数からあるa
にm b
して、id = return
とし(.) = (<=<)
。それは正しいですか、それとも私はさまざまなレベルのことを混同していますか?
a
との間にありますb
が、単純な関数ではありません。それらはm
、関数の戻り値の追加で装飾されています。
型システムは、プログラム内の用語の実行時の動作に対する一種の静的近似を計算するものと見なすことができます。
そのため、強力な型システムを備えた言語は、型の不適切な言語よりも厳密に表現力があります。同じ方法でモナドについて考えることができます。
@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
、そのようにi <= 10, sum [1..i] > 4, sum [1..i] < 25
、
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
そして最後の例として、filterAccum
は次のように定義できますfilterM
。
filterAccum f a xs = evalState (filterM (state . flip f) xs) a
StateT
普通のデータ型である、フードの下で使用されるモナド、。
この例は、モナドによって計算コンテキストを抽象化し、@ Carlが説明するようにモナドの構成可能性によりクリーンな再利用可能なコードを作成できるだけでなく、ユーザー定義のデータ型と組み込みプリミティブを均一に扱うことを示しています。
IO
特に優れたモナドと見なすべきではないと思いますが、初心者にとっては驚くべきモナドの1つであることは間違いないので、説明に使用します。
純粋に関数型の言語(実際にはHaskellが最初に使用した言語)で考えられる最も単純なIOシステムは次のとおりです。
main₀ :: String -> String
main₀ _ = "Hello World"
怠惰な場合、その単純な署名は、実際にはインタラクティブな端末プログラムを構築するのに十分ですが、非常に制限されます。最もイライラするのは、テキストしか出力できないことです。さらにエキサイティングな出力の可能性を追加した場合はどうなりますか?
data Output = TxtOutput String
| Beep Frequency
main₁ :: String -> [Output]
main₁ _ = [ TxtOutput "Hello World"
-- , Beep 440 -- for debugging
]
かわいいですが、もちろん、より現実的な「代替出力」はファイルへの書き込みです。ただし、ファイルから読み取る方法も必要です。万が一?
ええと、main₁
プログラムを取得して単純にファイルをプロセスに(オペレーティングシステム機能を使用して)パイプするとき、基本的にファイル読み取りを実装しました。Haskell言語内からそのファイル読み取りをトリガーできるとしたら...
readFile :: Filepath -> (String -> [Output]) -> [Output]
これは、「インタラクティブプログラム」を使用String->[Output]
して、ファイルから取得した文字列をフィードし、指定されたプログラムを単に実行する非インタラクティブプログラムを生成します。
ここに問題が1つあります。ファイルがいつ読み取られるかについては、私たちには本当に概念がありません。[Output]
リストは確かに素敵な順序を与える出力が、我々はときにするため得ることはありません入力が行われますが。
解決策:行うべきことのリストの項目にもinput-eventsを作成します。
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"]
]
さて、不均衡に気づくかもしれません。ファイルを読み取って出力をそれに依存させることができますが、ファイルの内容を使用して、たとえば別のファイルを読み取ることもできません。明白な解決策:input-eventsの結果も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の効率的な実装ではありませんが、原則として使用可能です。
IO3 a ≡ Cont IO2 a
。しかし、私はそのコメントを、継続モナドをすでに知っている人たちへのうなずきとして、より正確に初心者に優しいという評判がないので、それを意味しました。
モナドは、繰り返し発生する問題のクラスを解決するための便利なフレームワークにすぎません。まず、モナドはファンクタである必要があります(つまり、要素(またはそのタイプ)を調べずにマッピングをサポートする必要があります)。また、バインディング(またはチェーン)操作と、要素タイプからモナド値を作成する方法(return
)も必要です。最後に、bind
そしてreturn
また、モナドの法則と呼ばれる2つの方程式(左と右のアイデンティティを)、満たさなければなりません。(あるいは、モナドをflattening operation
バインディングの代わりにを持つように定義することもできます。)
リストモナドは、一般的に非決定論に対処するために使用されます。バインド操作は、リストの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セッションの例を示します。
*Main> perm "a"
["a"]
*Main> perm "ab"
["ab","ba"]
*Main> perm ""
[]
*Main> perm "abc"
["abc","acb","bac","bca","cab","cba"]
リストモナドは決して計算に悪影響を与えるものではないことに注意してください。モナドである数学的構造(つまり、上記のインターフェースと法則に準拠)は副作用を意味しませんが、副作用現象はモナドフレームワークにうまく適合します。
モナドは基本的に、関数をチェーンで構成するのに役立ちます。限目。
現在、それらの作成方法は既存のモナドによって異なり、その結果、異なる動作になります(たとえば、状態モナドで可変状態をシミュレートするため)。
モナドに関する混乱は、非常に一般的、つまり関数を作成するメカニズムであり、多くのものに使用できるため、モナドは「関数の構成」についてのみであるのに、モナドは状態やIOなどについてであると人々に信じ込ませます。 」
さて、モナドの興味深い点の1つは、コンポジションの結果は常に「M a」タイプ、つまり「M」でタグ付けされたエンベロープ内の値であることです。この機能は、たとえば、純粋なコードと純粋でないコードを明確に区別して実装するのに本当に便利です。すべての純粋でないアクションを「IO a」型の関数として宣言し、IOモナドを定義するときに、「 「IO a」内の「a」値。結果は、純粋なままでいる間はそのような値を取得する方法がないため(使用する関数は「IO」モナド内にある必要があるため)、純粋な関数はなく、同時に「IO a」から値を取得できません。そのような値)。(注:まあ、完璧なものは何もないため、「unsafePerformIO:IO a-> a」を使用して「IO拘束ジャケット」を壊すことができます
型コンストラクターと、その型ファミリーの値を返す関数がある場合は、モナドが必要です。最終的には、これらの種類の機能を組み合わせる必要があります。これらは、その理由に答えるための 3つの重要な要素です。
詳しく説明します。あなたは持っているInt
、String
とReal
し、タイプの機能Int -> String
、String -> Real
およびオンそう。これらの関数は簡単に組み合わせることができ、末尾はInt -> Real
です。人生は素晴らしい。
その後、ある日、新しいタイプのファミリーを作成する必要があります。値を返さない(Maybe
)、エラーを返す(Either
)、複数の結果(List
)などの可能性を考慮する必要があるためです。
これMaybe
は型コンストラクタです。のような型を取り、Int
新しい型を返しますMaybe Int
。最初に覚えておかなければならないのは、型コンストラクターもモナドもありません。
もちろん、あなたはあなたのタイプのコンストラクタを使用したい、あなたのコード内で、すぐに次のような機能で終わるInt -> Maybe String
とString -> Maybe Float
。これで、関数を簡単に組み合わせることができなくなりました。人生はもう良くありません。
そして、ここでモナドが助けに来ます。それらを使用すると、そのような機能を再び組み合わせることができます。あなただけの構成を変更する必要があります。用> ==。