2022/5/21 関数適用の表現、その他稚拙な表現を修正。
最近、プログラミングにおけるモナドについて調べていて、モナドと言うものがどんなものなのか掴み始めてきて、ある程度これが誤解なのかそうでないのか見分けられるようになってきたので、モナドのよく見かける誤解について書いていく。
前置き
情報を調べていくと、結構モナドに関する情報が錯綜していることがわかる。筆者もモナドがどういうものなのか掴み始めてくるまで、本当に何が何なのかわからなくてしょうがなかった。しかし今ならある程度はわかる。その原因はモナドに関する誤解を招くような記事が多すぎることにあったのではないか、と。
そこでできるだけ数学的知識を使わず、初心者の方にもある程度わかりやすいようにモナドのあらゆる誤解を解く記事を書いていきたいと思う。
注意として、筆者はモナド初心者であり、また数学におけるモナドは1ミリも理解していないので、数学的な証明は行わず、この記事でも誤った情報がある可能性のあることを示唆しておく。
極力誤解を解く記事にしていきたいので、コメントもバンバンしてほしい(承認制ですが、誹謗中傷じゃなければ普通に承認しますのでご安心ください)。ただし圏論的な説明は勘弁してほしい(´・ω・`)。
誤解1:ファンクタ、アプリカティブはモナドである
ファンクタはモナドではなく、アプリカティブもモナドではない。
プログラミングにおけるモナドとは、以下の3つの組を満たす構造のことである。
型コンストラクタM |
1つの型を"引数"として取り構築する型 |
return 関数*1 |
a 型の値からM a 型の値に変換する |
(>>=) 演算子*2 |
M a 型の値からa 型の値に変換し、M b 型を得る関数を適用する例) [1,2,3] >>= (λx. return (x*3)) --> [3,6,9] |
それに対してファンクタとアプリカティブは以下の通りとなっている。
型コンストラクタF |
1つの型を"引数"として取り構築する型 |
fmap 関数*3 |
(a -> b) 型の関数をF a 型の値からF b 型の値に変換する関数となるように対応づける例) fmap (λx. x+3) Just 1 --> Just 4 |
型コンストラクタA |
1つの型を"引数"として取り構築する型 |
pure 関数 |
a 型の値からA a 型の値に変換する |
(<*>) 演算子*4 |
A (a -> b) 型のすべての関数をA a 型の値に適用しA b 型の値を得る関数になるよう対応づける例) [(+),(*)] <*> [1,2] <*> [3,4] --> [4,5,5,6,3,4,6,8] |
このように、多少似ているところはあっても、ファンクタとアプリカティブはモナドとは性質的に異なるものであることが見て取れる。
もう少し詳しく見ていこう。
ファンクタのfmap
とモナドの(>>=)
演算子を比較したコードを以下に示す。
-- ファンクタのfmap fmap (λx. x+3) [1,2,3] ---> [4,5,6] -- モナドの(>>=) [1,2,3] >>= (λx. return (x+3)) ---> [4,5,6]
上記のコードを見ると、挙動は似ている。しかし、ファンクタのfmap
はモナドの(>>=)
のように
m >>= f >>= g
と言った次の計算に連結させるような計算ができないため、モナドよりも表現力は低い。また、そもそもファンクタにはモナドのreturn
関数に相当するものはないため、この時点でファンクタはモナドとは言えないだろう。
そしてアプリカティブだが、アプリカティブにはモナドのreturn
関数とまったく挙動が同じであるpure
関数があり、また(<*>)
演算子は
-- アプリカティブの(<*>) pure (λx. x+3) <*> [1,2,3] ---> [4,5,6] -- モナドの(>>=) [1,2,3] >>= (λx. return (x+3)) ---> [4,5,6]
のように使うこともできる。したがって、モナドの(>>=)
演算子と挙動が似ており、モナドとアプリカティブは非常によく似ていると言える。さらに、アプリカティブの(<*>)
は以下のように書けば、モナドが目指すところの手続き的な計算もある程度可能となる。
pure (const id) <*> putStr "Hello, " <*> putStrLn "World!"
これはアプリカティブ・スタイルと呼ばれているらしい(参考:すごいHaskellたのしく学ぼう!, Applicativeのススメ - あどけない話, Applicative スタイル `f <$> m1 <*> m2` を読み解く - Qiita)。
しかし、惜しくもこれはモナドではない。なぜならば、
putStrLn "Input 2-values: " >>= λ_. getLine >>= λx. getLine >>= λy. putStrLn ("result: " ++ show ((read x :: Int) + (read y :: Int)))
のように、ファンクタと同様にアプリカティブの(<*>)
も次の計算につなげるような計算はできない(あくまで可能なのは、関数適用を利用した計算)のでこれもまたモナドよりも表現力が低いと言えるからである。
よってアプリカティブもモナドとは言えないだろう。
以上から、ファンクタとアプリカティブはモナドではない。
ちなみに純粋関数型言語HaskellのコンパイラであるGHCでは現在、Functor
がApplicative
の必要条件であり、Applicative
がMonad
の必要条件と定義されている。
つまりHaskellにおいては、Monad
に含まれる型は必然的にFunctor
、Applicative
にも含まれる。しかしHaskellにおいても、Functor
、Applicative
はMonad
の十分条件ではない(型がFunctor
やApplicative
に含まれていても、Monad
に含まれているとは限らない)ため、ファンクタとアプリカティブはモナドではないと言うことができる。
誤解2:プログラミングにおけるモナドは数学におけるモナドと等価である
プログラミングにおけるモナドと、数学におけるモナドは厳密には異なる。
先ほど説明したが、プログラミングにおけるモナドとは、「型コンストラクタM
」「return
関数」「(>>=)
演算子」の3つの組である。
それに対して、数学におけるモナドはどうだろうか。数学におけるモナドを先ほどと同様に書く*5と、以下のようになると思われる。
型コンストラクタT |
1つの型を"引数"として取り構築する型 |
return 関数 |
a 型の値からT a 型の値に変換する |
join 関数 |
T (T a) 型の値からT a 型の値に変換する |
「型コンストラクタT
」「return
関数」までは同じであるものの、「(>>=)
演算子」の代わりに「join
関数」が定義されている、というところが異なる。
数学におけるモナドとはどういったものなのか?
自分は数学におけるモナドについても圏論を知らないなりに調べてみた結果、「T
で包んだり、T
を外したりすることができる」という性質を持っていることがわかった。
まずreturn
関数ではa
型に対してT
で包み、T a
型にして返している、と考えることができる。
a =[ return ]=> T a
このa
型というのは任意の型である、ということを示すので、T a
型とすることもできる。つまり、return
関数ではT a
型からさらにT
で包んでT (T a)
にすることもできるのである。
a = T aであるとき、
T a =[ return ]=> T (T a)
しかしreturn
関数だけでは何かと不便である。何重にもT
で包んだ型からT
を外す関数も欲しい。
そこで使われるのがjoin
関数である。
join
関数はT
で包んだ型からT
を外す関数である、と考えることができる。しかし制約条件があり、二重以上T
に包まれていなければ外すことはできない。
T (T a) =[ join ]=> T a
これが数学におけるモナドの特徴であると思われる。
そしてプログラミングにおけるモナドの(>>=)
演算子は、join
関数とは異なり、T
で包まれた型からT
を外し、次の最終的にT
に包まれた型にする何らかの計算にうつす関数だと考えられる。
M a =[ >>= ]=> a =[ 何らかの計算 ]=> M b
このように定義されている3つの組が異なるので、この時点でプログラミングにおけるモナドは数学におけるモナドとは言い難いだろう。
しかしまったく無関係であるとは言えず、実はjoin
関数とそれに加えてファンクタのfmap
関数があれば(>>=)
演算子を定義することが可能なのである。その逆に、(>>=)
演算子でjoin
関数を定義することも可能である。
実際にHaskellでjoin'
関数を定義*6し、そこから独自に(>>=^)
演算子を定義してみる。
その前に、まず各関数のHaskellにおける型について知らなければなるまい。Haskellにおける各関数の型は、以下のとおりである。
join
関数
-- join関数 join :: Monad m => m (m a) -> m a
join
関数はm (m a)
型の値からm a
型の値に変換する関数であるため、型はm (m a) -> m a
となる。
(>>=)
演算子
-- (>>=)演算子 (>>=) :: Monad m => m a -> (a -> m b) -> m b
(>>=)
演算子はm a
型の値からa
型の値に変換し、(a -> m b)
型の関数を適用するため型はm a -> (a -> m b) -> m b
となる。
では実際に定義してみよう。
-- 素直な定義 join' :: Monad m => m (m a) -> m a join' m = m >>= id -- idは恒等関数であり、(a -> a)型である -- ポイントフリースタイル join' :: Monad m => m (m a) -> m a join' = (>>=id)
join
関数はモナドの条件を満たした値に(>>=)
を部分適用し、その関数をid
関数に適用するだけで定義できる。id
関数とは、いわゆる数学における恒等関数であり、値に適用しそのまま値を得る。なんでそんなことをするの?と思われるかもしれないが、これは途中に適用する(a -> m b)
型の関数を見てみるとわかる。
先ほども書いた通り、a
型は任意の型である、ということを示す。つまりa = m a
であると考えると、
a = m aであるとき、
m a =[ (a -> m b)型の関数 ]=> m b
となるため、値をそのまま返す恒等関数が最も適した関数であると言えるのである。
これにより、定義した関数を適用する値はm (m a)
型でなければならなくなった。なぜなら、(>>=)
演算子の途中で適用する(a -> m b)
型の関数はm b
型の値、すなわちモナドの条件を満たした値を得なければならないためである。
以上から、m >>= id
はjoin
関数をm
に適用したのと等価となる。
次に、join'
関数から(>>=^)
演算子を定義してみる。
-- 素直な定義 infixl 1 >>=^ (>>=^) :: Monad m => m a -> (a -> m b) -> m b m >>=^ f = join' (fmap f m) -- こういう素直(?)な定義もできる infixl 1 >>=^ (>>=^) :: Monad m => m a -> (a -> m b) -> m b m >>=^ f = join' $ f <$> m -- ($)は関数適用演算子、(<$>)はfmapと等価 -- ポイントフリースタイル infixl 1 >>=^ (>>=^) :: Monad m => m a -> (a -> m b) -> m b (>>=^) = (join' .) . flip fmap
まずjoin'
はm (m a) -> m a
型であり、関数に適用する値の型はm (m a)
でなければならない。さらに、(>>=^)
演算子の途中で適用する関数の型は(a -> m b)
でなければならない。
しかし、(>>=^)
で最初に部分適用するのはm a
型の値に対してである。それをどうやって(a -> m b)
型の関数を適用できるように持っていくのか?
そこでファンクタのfmap
関数の出番である。fmap
関数の型は以下のようになっている。
-- fmap関数 fmap :: Functor f => (a -> b) -> f a -> f b
ここで、b
型は任意の型であることに注目してみる。すると、b
型はf b
型であると考えることでfmap関数の型は(a -> f b) -> f a -> f (f b)
とすることができるのである。
そしてjoin'
関数の型を思い出してみよう。そう、(m (m a) -> m a)
である。これをfmap
から得たf (f b)
型の値に適用してやると、join'
関数からf b
型の値を得ることができ、これがm b
となる。
以上から、「fmap f m
を評価してからその結果にjoin
関数を適用する」関数をm
とf
に適用することは、m >>= f
と等価となる。
ここから数学におけるモナドの性質は「包んだり、多重になったものを外したりする」であるのに対し、プログラミングにおけるモナドの性質は「包んだり、外したりする」ことに加え、「外したうえで関数適用を行い、その結果を包む」ことができるというものなので、似てはいるが、性質としては異なることがわかるだろう。
したがって、プログラミングにおけるモナドと数学におけるモナドは「密接な関係はある」が、「等価」であるとは言えない。だから「『プログラミングにおけるモナド』と『数学におけるモナド』は等価である」と言うのは厳密には間違っているだろう。
ちなみにプログラミングにおけるモナドは、数学的には「Kleisli triple(クライスリ・トリプル)」と呼ばれていて、(a -> m b)
型の関数は「Kleisli(クライスリ)射」と呼ばれているらしい。
これは完全に蛇足だが、豆知識ということで書いておく。
誤解3:Stateモナドでは破壊的代入が行われている
ここからは具体的なモナドについて書いていく。
State
モナドは「破壊的代入」をしている、という情報もあったので、書いておくと、これも違う。State
モナドはあくまで「疑似的」な破壊的代入を表現するためのモナドに過ぎない。より正確に言えば、「状態」から「実行結果」と「新しい状態」を返すこと、いわば「状態付き計算」を表現したState
型を、モナドに適応しただけに過ぎないのである。
そしてState
モナドに関連して、IO
モナドについても書いておく。なんでかと言うと、IO
モナドは非常にざっくり説明すると「現実世界*7を状態としている特殊なState
モナド」だからである(参考:Haskell の IO モナドと参照透過性の秘密 - TIM Labs, IO モナドと副作用 - Haskell-jp)。
こちらもやっていることがState
モナドとほぼ同じなので破壊的代入は行われていない。したがって、「State
モナドとIO
モナドでは破壊的代入が行われている」と言われるのは、完全に嘘である。「『破壊的代入』を表現している」とは言えるが、実際には行われていない。
終わり。大体見てきて誤情報ではないかと思った情報はこれぐらいであるが、誤情報に関してはまだまだあるかもしれない。