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!"
これはアプリカティブ・スタイルと呼ばれているらしい(参考:, 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モナドでは破壊的代入が行われている」と言われるのは、完全に嘘である。「『破壊的代入』を表現している」とは言えるが、実際には行われていない。
終わり。大体見てきて誤情報ではないかと思った情報はこれぐらいであるが、誤情報に関してはまだまだあるかもしれない。