Haskellを学んでいる間、モナドとは何か、Haskellでモナドが重要である理由を説明しようとする多くのチュートリアルに直面しました。それぞれが類似性を使用しているので、意味を理解しやすくなります。結局のところ、私はモナドが何であるかについて3つの異なる見方をしている:
ビュー1:ラベルとしてのモナド
時々、モナドは特定のタイプのラベルとして考えます。たとえば、次のタイプの関数:
myfunction :: IO Int
myfunctionは、実行されるたびにInt値を生成する関数です。結果のタイプはIntではなく、IO Intです。したがって、IOはInt値のラベルであり、Int値がIOアクションが実行されたプロセスの結果。
したがって、このInt値は、IOのプロセスからの値としてマークされているため、この値は「ダーティ」です。プロセスはもはや純粋ではありません。
ビュー2:モナドは厄介なことが発生する可能性があるプライベートスペースです。
すべてのプロセスが純粋で厳密なシステムでは、副作用が必要になる場合があります。したがって、モナドは、厄介な副作用を起こすための小さなスペースです。この空間で、あなたは純粋な世界から脱出し、不純に行き、あなたのプロセスを作り、そして価値を取り戻すことができます。
ビュー3:カテゴリ理論のモナド
これは私にはよくわからない見方です。モナドは同じカテゴリまたはサブカテゴリの単なるファンクタです。たとえば、Int値があり、サブカテゴリIO Int、つまりIOプロセスの後に生成されたInt値です。
これらのビューは正しいですか?どちらがより正確ですか?
ビュー#1と#2は一般的に正しくありません。
* -> *
_のデータ型はラベルとして機能します。モナドはそれ以上のものです。IO
モナドを除いて)モナド内の計算は不明瞭ではありません。これらは単に副作用があると認識している計算を表すものですが、純粋なものです。これらの誤解はどちらも、実際には少し特殊なIO
モナドに焦点を合わせたことから生じます。
#3については、できればカテゴリー理論に触れずに、少し詳しく説明します。
標準計算
関数型プログラミング言語でのすべての計算は、ソースタイプとターゲットタイプを持つ関数と見なすことができます:_f :: a -> b
_。関数に複数の引数がある場合、 currying で1つの引数の関数に変換できます( Haskell wiki も参照)。そして、値_x :: a
_(引数が0の関数)がある場合、それを ユニットタイプ :_(\_ -> x) :: () -> a
_の引数を取る関数に変換できます。
_.
_演算子を使用してそのような関数を作成することにより、より複雑なプログラムをより単純なプログラムから構築できます。たとえば、_f :: a -> b
_と_g :: b -> c
_がある場合、_g . f :: a -> c
_を取得します。これは変換された値でも機能することに注意してください。_x :: a
_があり、それを表現に変換すると、f . ((\_ -> x) :: () -> a) :: () -> b
が得られます。
この表現には、次のような非常に重要な特性があります。
id :: a -> a
_の各型a
には、非常に特殊な関数があります。 _.
_に関しては 識別要素 です。f
は_f . id
_と_id . f
_の両方に等しいです。.
_は associative です。単項計算
結果に単一の戻り値以外のものが含まれる、特別な計算のカテゴリを選択して操作したいとします。 「もっと何か」の意味を特定するのではなく、できるだけ一般的なものにしたい。 「もっと何か」を表現する最も一般的な方法は、型関数として_* -> *
_型のm
型として表現することです(つまり、ある型を別の型に変換します)。したがって、処理したい計算のカテゴリごとに、型関数_m :: * -> *
_を用意します。 (Haskellでは、m
は_[]
_、IO
、Maybe
などです)。また、カテゴリには_a -> m b
_型のすべての関数が含まれます。
ここでは、基本的なケースと同じ方法で、このようなカテゴリの関数を操作します。私たちはこれらの機能を構成できるようにしたい、その構成を連想的にしたい、そしてアイデンティティを持っていたい。必要なもの:
<=<
_および_f :: a -> m b
_を_g :: b -> m c
_のようなものに構成する演算子(これを_g <=< f :: a -> m c
_と呼びましょう)を持つにはそして、それは連想的でなければなりません。return
と呼びます。また、_f <=< return
_がf
と同じで、_return <=< f
_と同じであることも必要です。そのような関数return
および_m :: * -> *
_がある_<=<
_は、monadと呼ばれます。基本的な場合と同様に、単純な計算から複雑な計算を作成できますが、戻り値の型はm
によって変換されます。
(実際、私はここでcategoryという用語を少し乱用しました。カテゴリ理論の意味では、これらの法律に準拠していることを知った後でのみ、私たちの構築をカテゴリと呼ぶことができます。)
Haskellのモナド
Haskell(および他の関数型言語)では、主に値を処理しますが、_() -> a
_型の関数は処理しません。したがって、モナドごとに_<=<
_を定義する代わりに、関数_(>>=) :: m a -> (a -> m b) -> m b
_を定義します。このような代替定義は同等であり、_>>=
_を使用して_<=<
_を表現できます(その逆も可能です)(演習として試すか、または ソース を参照)。原理は今やあまり明白ではありませんが、同じです。結果は常に_m a
_型であり、_a -> m b
_型の関数を作成します。
作成するモナドごとに、return
と_<=<
_に必要なプロパティ(結合性と左/右ID)があることを確認することを忘れないでください。 return
および_>>=
_を使用して表現すると、それらは モナド則 と呼ばれます。
例-リスト
m
を_[]
_として選択すると、_a -> [b]
_型の関数のカテゴリが取得されます。このような関数は非決定的な計算を表し、その結果は1つ以上の値になる可能性がありますが、値がない場合もあります。これは、いわゆる listモナド を引き起こします。 _f :: a -> [b]
_と_g :: b -> [c]
_の構成は次のように機能します。_g <=< f :: a -> [c]
_は、タイプ_[b]
_のすべての可能な結果を計算し、それぞれにg
を適用して、すべての結果を単一のリスト。ハスケルで表現
_return :: a -> [a]
return x = [x]
(<=<) :: (b -> [c]) -> (a -> [b]) -> (a -> [c])
g (<=<) f = concat . map g . f
_
または_>>=
_を使用
_(>>=) :: [a] -> (a -> [b]) -> [b]
x >>= f = concat (map f x)
_
この例では、戻り値の型が_[a]
_だったため、a
型の値が含まれていない可能性があることに注意してください。実際、モナドには、戻り値の型がそのような値でなければならないという要件はありません。モナドの中には常に(IO
やState
など)持っているものもありますが、_[]
_やMaybe
のように持っていないものもあります。
IOモナド
すでに述べたように、IO
モナドはやや特殊です。タイプ_IO a
_の値は、プログラムの環境との対話によって構築されたタイプa
の値を意味します。したがって、(他のすべてのモナドとは異なり)、純粋な構造を使用して_IO a
_と入力します。ここでIO
は、環境と相互作用する計算を区別するタグまたはラベルです。これが(唯一のケース)ビュー#1と#2が正しい場合です。
IO
モナドの場合:
f :: a -> IO b
_と_g :: b -> IO c
_の構成は、環境とやり取りするf
を計算し、次に値を使用してg
を計算し、とやり取りする結果を計算する環境。return
は、IO
"タグ"を値に追加するだけです(環境をそのままにして、結果を "計算"します)。いくつかのメモ:
m a
_の結果型を持っているため、IO
モナドから「エスケープ」する方法はありません。意味は次のとおりです。計算が環境と相互作用すると、環境から相互作用を構築することはできません。IO
内のいくつかのステートフルな計算によって、(lastリゾートとして)タスクをプログラムできますモナド。これがIO
がプログラマのsin binと呼ばれることが多い理由です。getChar
などの関数の結果の型は_IO something
_でなければなりません。「その結果、このInt値は、IOのプロセスからの値としてマークされているため、この値は「ダーティ」です。」
「IO Int」は一般的にInt値ではありません(「return 3」などの場合もあります)。何らかのInt値を出力する手続きです。この「手順」を実行すると、Int値が異なる場合があります。
モナドmは、埋め込まれた(命令型の)「プログラミング言語」です。この言語内では、いくつかの「手順」を定義できます。モナド値(タイプm aの)は、この「プログラミング言語」のタイプaの値を出力する手続きです。
例えば:
foo :: IO Int
int型の値を出力するいくつかのプロシージャです。
次に:
bar :: IO (Int, Int)
bar = do
a <- foo
b <- foo
return (a,b)
2つの(おそらく異なる)Intを出力するいくつかの手順です。
そのような「言語」はすべて、いくつかの操作をサポートしています。
2つのプロシージャ(maとmb)を「連結」することができます。最初のプロシージャと2番目のプロシージャで構成されるより大きなプロシージャ(ma >> mb)を作成できます。
さらに、最初の出力(a)が2番目の出力(a >> =\a-> ...)に影響を与える可能性があります。
プロシージャ(return x)は定数値(x)を生成する場合があります。
さまざまな組み込みプログラミング言語は、サポートする種類の点で異なります。
モナド型とモナドクラスを混同しないでください。
モナディックタイプ(つまり、モナドクラスのインスタンスであるタイプ)は、特定の問題を解決します(原則として、各モナディックタイプは異なる問題を解決します):State、Random、Maybe、IO。それらはすべてコンテキスト付きのタイプです(「ラベル」と呼ぶものですが、それがモナドになるわけではありません)。
それらすべてに対して、「選択による操作のチェーン」の必要があります(1つの操作は前の結果に依存します)。ここにモナドクラスが登場します:(与えられた問題を解決する)型をモナドクラスのインスタンスにし、連鎖問題を解決します。