予は如何にしてMonadophobiaを克服しつつあるか
Monadについて実は数日前にようやく理解しはじめた。思えば10年前に最初にそれが函手(functor)だって教えてくれれば、少なくとも簡単なイメージ形成と基礎的な使い方はすぐに理解できたのに、と思う。HaskellのMonadを理解するために圏論を理解する必要はありません、ってのは本当かもしれないが、函手だって言われれば直観的にはすぐに理解できる。要は fmap はHaskellの函数全体が作る圏からその部分圏への函手です、fmapで函数の構造を保存したまま部分圏の函数が得られますよ、というか函手の条件を満たすようなfmapを提供することによってそういう構造保存的な部分圏を提供するんですよ、というだけのことである。ところが、部分圏の射自体がHaskellの圏の射だから、当の函手が自分自身に適用されてm (m a)のようにMonadのマトリョーシカを作ってしまう。Functorはまさにこれである。でも、入れ子を外せないと不便なので(リスト操作でしょっちゅうconcatにお世話になっていることを考えればそれは明らかだろう)、fmapに加えて入れ子を外す(というか自分に属する射を自分に属する射に移すというか)joinを提供する構造もお付けしました、というのがMonadなわけでしょ。なお、数学苦手な文系情弱なので圏論風味の用語が不正確なのは見逃してくれなさい。
どうやって納得したか
で、MonadってのはHaskellの函数の構造を内部に保存してくれるので使い勝手がいいステキなタイプのデータ型の中でも特に便利なやつのことです、でいいではないか。fmapがなかったら普通のデータ型なんだから、a->b型の函数があったとしても、それを再利用したりはできず、m a -> m b版の函数を全部一から定義しなきゃいけなくて死ぬほど面倒でしょ、っていう辺りの説明でいいじゃん。だからMonad(というかこの段階ではFunctor)が必要なんだ、と。
even (:: Integer -> Bool)を考えよう。Maybe型の世界の中の値、つまりJust 10についてもevenを使って偶数か奇数かを判定したいのは理の当然である。しかし、Just 10はMaybe Integer型だから、evenは適用しようがない。そこでJust 10に適用されてJust Trueを返す函数が、つまりMaybeの世界の中の値に対してevenと同じ働き方をする函数が、あればいい(扱う対象がMaybeの世界の中の値たちだというだけでこの関数自体はMaybeの世界の外側の住人である)。
meven :: Maybe Integer -> Maybe Bool
meven Just x = Just (even x)
meven Nothing = Nothing
これはうまくいく。だが、evenのみならずHaskellのあらゆる(Maybeの外側の世界の)函数について、こんな定義はしていられないだろう。こんな関数定義をすることなく、evenを取ってmevenを返してくれるような函数が欲しい。これがfmap(その中置演算子版が<$>)にほかならない。つまり:
fmap :: (a->b) -> ( m a -> m b)
fmap even :: m Integer -> m Bool
fmap even (Just 10)
=> Just True
even<$>(Just 15) -- fmapの中置演算子版 <$>を使うと$を使った関数適用風に
=> Just False
even<$>(Nothing)
=> Nothing
である。ここまではわかりやすい。fmap超便利、である(というかこういうfmapをMaybe型が備えているというのがMaybe型を超便利にしているわけである)。evenだろうがなんだろうがどんな函数であっても、引数にNothingがきたらNothingを返す新たな函数を作り上げる辺りもfmapが面倒を見てくれている(というかそういう面倒を見るようにfmapを定義するのがMaybe型の作者の責任である)。だがここで話は終わらない。evenのようにひとつの引数をとってひとつの値を返すのはいいが、多変数だったら(つまり高階関数だったら)どうなるだろうか。そこでだ、Just 20をJust 10で除してJust 2が欲しい時に(divにfmapを使えば良さそうに思えるわけだが)、fmapを使ってみるとどうなるか見てみよう。単純にdivにfmapを適用してやればいいかというに:
div :: Integer -> (Integer -> Integer)
(<$>) :: (a -> b) -> (m a -> m b)
div <$> :: m Integer -> m (Integer -> Integer)
なのでJust 20を部分適用してやると、その結果は
mdiv20 = div <$> (Just 20)
mdiv20 :: Maybe (Integer -> Integer)
である。Just 20を部分適用したからには、ここで欲しいのはJust 10を取ってJust 2を返してくるような函数なわけだから、その型は(Maybe Integer -> Maybe Integer)でなくてはならない。だが、mdiv20の型はMaybe (Integer -> Integer)なのであって、これは似て非なるものである。だからmdiv20にもう一度手を加える必要がある。しかしながら、これにfmapを適用しようとしてもできないのだ。それも当たり前で、これは函数ではなくて函数を中に包んだMaybe型だから。Maybe型を中に分配してくれるような(これはある意味では一定の制約の下でMaybe型を外しているとも言える)、fmapに似たなにかが必要だ。Maybe型の世界の中に閉じ込められた函数をMaybe型に対する函数というこちらの世界のなにかに変えてくれるなにかが。ここで試しに函数適用演算子$にfmapを適用してみてもムダである:
($) :: (a->b) -> (a->b)
($)<$> :: m (a->b) -> m (a->b)
ご覧の通り、これを使ってもmがm a -> m bという風に中に入っていってはくれない。というわけで、結局のところfmap, <$>だけではどうやってもこれはムリだ、ということになる。外側の世界の高階関数をマトモに使えるものとしてmの世界に持ち込みたければ、まさにこれをやってくれる更なる「なにか」つまり次のような演算子<\*>が必要で:
(<*>) :: m (a -> b) -> (m a -> m b)
これを持っている特別なFunctorがApplicativeだというわけなのだな。実際、
(<*>)mdiv20 :: Maybe Integer -> Maybe Integer
で:
div <$> (Just 20) <*> (Just 10)
=> Just 2
となる。実にめでたい。<$>は要するに (<\*>).returnだから(本当はreturnではなくpureだが)、こうしてみるとわざわざApplicativeじゃないFunctorとか使い道なさそうなので、最初からApplicativeでいいじゃん、という(その割にわざわざimportしないといけないのがまた釈然としないけど)。同じことだが:
(pure div)<*>(Just 20)<*>(Just 10)
=> Just 2
つまりpure/returnによってdivを最初にMaybe型の中にぶち込む(その型は Maybe (Integer -> Integer -> Integer)である)。これを<*>が受け取って、Maybe Integer -> Maybe (Integer -> Integer)型の、divに対応するある函数を返してくる。この函数がJust 20に適用されて、Maybe (Integer -> Integer)型の値が返ってくる(これがさっきのmdiv20だ)。これを<*>が受け取って、Maybe Integer -> Maybe Integer 型の対応物を返してくる。この函数がJust 10に適用されて、めでたくJust 2が返ってくる。函数自体がMonad mに包まれてしまっている時に(なのでそれはいまや函数ではない)、<*>(別名ap)がそれをMonad mの値に対して無理やり「適用 apply」するという仕掛け。この一連のやり方でMonad mを弄り回すのがApplicativeスタイルというわけなのだな。
こっから更にMonadに辿り着くためには自己函手と自然変換の話が必要なのはわかっているが、ここでやめておいてまたあとでゆっくり考える。しかしともかく、こういう説明を最初にされていれば10年前の挫折はなかったのである。
それにしても
IO Monadを例にとった上に do記法を最初に見せて、しかもそれが糖衣構文で、その正体は(>>=)です、とか言われてわかるわけがねえだろうがよ、と言いたい。ムリ。2001年辺りだったと思うが、Haskellに最初に触れた時にMonadが理解できずに挫折して完全に遠ざかり、10年経って戯れに戻ってみたらすんなりと理解できるのは、私のオツムが少しはマシになったからなのか、Haskell界に於けるMonadについての説明が発達したからなのか。もちろんほぼ間違いなく後者なのであり、実にありがたいことである。でもdo構文考えだしたりid(::a->a)からid(::m a -> m a)の射にreturnとかいう名前をつけたりして命令型風味の構文考えたりした奴は万死に値すると思うね。最初からきちんと函手としての説明をすべきだった。余計なことをするから私のような挫折者が出るのである。