The Road to Monads: Monadについて

この記事はThe Road to Monadsを意訳したものであり、RICORA Advent Calendarの11日目の記事です。

Monad

今までの流れを振り返ると、Functorは何らかの値のWrapperであり、fmapによってその要素に関数適用ができた。 Applicative Functorはそれをさらに発展させて、関数自体をFunctorで包むことによって、複数の引数を取る関数を実装できるようにしていたのだった。

さて、何が足りないのだろう? 実は、Applicative Functorだけでは関数を複数組み合わせることが難しい。 例えば、f :: a -> bg :: b -> cがあったときに、Applicative Functorだとa -> f bb -> f cに写すことはできてもa -> f cに写すことができない。 こういう関数の使い方をする機会は割と多い気がするし、こんな感じで書きたくない?という話。

簡単にいえば、MonadはFunctorの中での関数合成を可能にする構造。 しかし、これだけだとMonadが実際に何をするかいまいちわからないので以下に書いていく。 導入として、大体FunctorみたいなものだとしてMonadmを使うことにしよう(とはいっても、実装上は拡張したApplicative型なんだからFunctorの拡張として捉えるほうが適切かもしれない)。

じつはMonadは他の必要っぽいことも包摂してくれている。

  1. Monad上の関数の繰り返し適用に対して、Monadを圧縮する。a -> m bに対してまたfmapをつかってmを適用すると、m a -> m (m -> b)になってしまう。 例えば、a -> Maybe bならMaybe a -> Maybe ( Maybe b )的な。 これを圧縮して、繰り返し適用してもa -> Maybe bの型を作れるようにする。
  2. さっきの例。 a -> m bb -> m cからa -> m cの関数を作りだせる。
  3. 逆にm aが与えられたとき、a -> m bの関数を使えるようにする。

型の圧縮

まあこれだけだとよくわからないので、具体例を考えよう。 1 / sqrt (x)Maybeを返すとして考える。 この関数を二つの関数1 / xsqrt(x)の合成で考えるとすると、例えば1/0はundefinedなので、1 / xx = 0ならNothingを返したい。 これはsqrt(x)でも一緒。x < 0ならNothingを返したい。 そこで、これらを実際に書いてみるとこんな感じ。

safeInverse :: Double -> Maybe Double
safeInverse 0 = Nothing
safeInverse _ = Just(1/x)

safeSqrt :: Double -> Maybe Double
safeSqrt x = case signum x of
  (-1) -> Nothing
  _ -> Just (sqrt x)

ここでsignumは符号を返す関数。

まあこれだけで完結するんだったらいいんだけど、もっとでかいプログラムの一部に組み込むならMaybeの場合分けをし続けることになる。 めんどくさい犬の散歩みたいだ。 fmapをつかってもいいかもしれないけど、fmapを使うと1 / sqrt(x)の関数の型がa -> Maybe (Maybe b)になってしまう。 これをもっとでかい関数の一部として使うとすると…大変なことになりそうだ。

それを避けるために、joinという関数を考える。 Maybeに対してはこんな感じで書ける。

join :: Maybe (Maybe a) -> Maybe a
join (Just x) = x
join Nothing = Nothing

これは圧縮そのものだから、実際に1 / sqrt(x)を書くと

sqrtInverse :: Double -> Maybe Double
sqrtInverse = join ( fmap safeInverse (safeSqrt x))

こんな感じ。 確認だけど、fmap SafeInverse (safeSqrt x)Double -> Maybe (Maybe Double)型の関数なので、joinを使うとDouble -> Maybe Doubleになる。

このjoinは実際にこんな感じに一般化できるらしい。

join :: Monad m => m (m a) -> m a

関数の合成

このsqrtInverseを関数の合成の観点から見てみよう。 さっきと同じように普通の値を取ってMonad内部の値を返すような関数を二つ考えて、それらを普通の値を取ってMonadの値として返す関数を想定するとき、>=>という演算子を使うことができる。 例えばf :: a -> m bg :: b -> m cがあるとき、f >=> ga -> m cという型になる。 この>=>演算子は魚に似ている(?)のでfish演算子と呼ばれ、この合成自体はKleisli(クライスリ)合成というらしい。

実際にこのfish演算子が使われている所を見てみよう。

(>=>) :: (a -> Maybe b) -> (b -> Maybe c) -> (a -> Maybe c)
f >=> g = \x -> case f x of
  Just x -> g x
  Nothing -> Nothing

うん。 まず一行目はfish演算子を定義した通りの定義になっている。 そこで、二行目以降ではMonadがMaybeなので、f xJust xNothingを返し、Just xならunwrapしてg xを返すようにパターンマッチングしてあげている。

このfish演算子を実際にさっきのsqrtInverseに使おう。

sqrtInverse :: Double -> Maybe Double
sqrtInverse = safeSqrt >=> safeInverse

だいぶ短くなったし、これで明示的にパターンマッチングの処理をしなくてよくなったのでだいぶ見通しが良くなるはずだ。

でもこれにはまだ問題がある。 Monadの中の値を使うときに恒等射的な関数を考える必要が出てくるのだ。 例えばm aという値に対してm a -> aという関数を作り、fish演算子を使う必要がある。 これだとまた型に合わせるための関数を作って見通しがまた良くなくなってしまう。

実際にそういう例を見てみよう。 const a b = aという関数を考えれば、Just 5を計算するために

(const (Just 4) >=> (\x -> Just (x+1))) ()

とする必要がある。

ごちゃごちゃしててわかりにくいから型を確かめよう。 \x -> Just(x + 1)Int -> Maybe Intという型で、const Just 4Num a => a -> Maybe bという型だ。 あれ、constって引数2つ取るんじゃないの?と思うんだけど、それはカリー化されたものを見ているだけで、実際には1変数だけ取ることもできる(もちろん、const aconst a bとは違う関数)。

それで>=>が使えるが、型チェックをすると(const Just 4 >=> (\x -> x + 1)) :: Num c => b -> Maybe cなので、もう一つなにかの引数が必要っぽいことがわかるので、入れてあげるとJust 5が出る。 これはなんでもいい。 例えば'c'でも()でもいい。

連続的に計算させる

では、別の演算子(>>=) :: m a -> (a -> m b) -> m bを考えよう。 これをbind(演算子?)と呼ぶ。

Maybeに対して実装するとこんな感じ。

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
x (>>=) f = case x of
  (Just x') -> f x'
  Nothing -> Nothing

ここでxMaybe x'という型だとした。

さっきのsqrtInverseをまた拾ってこよう。

sqrtInverse :: Double -> Maybe Double
sqrtInverse x = safeSqrt x >>= safeInverse

他の例としてこういうのもある。

Just 3 >>= (\x -> Just (x + 1))

これはもちろんJust 4を返す。

実際、bindはfish演算子やjoinより使いやすい。 でも、結局bindでやっていることはfish演算子やjoinを使って遠回りしてできることと同じだ。

bind、fishとjoinからお互いを作れることを示そう。 ここでconstidという関数を使っているが、それぞれconst a b = aid a = aと定義される関数である。

思い返せば、それぞれの型はこんな感じであった。

join :: m (m a) -> m a
(>=>) :: (a -> m b) -> (b -> m c) -> (a -> m c)
(>>=) :: m a -> (a -> m b) -> m b

そこで、まずjoinを作ってみよう。

join x = x >>= id
join x = const x >=> id

上はm (m a)型の変数を取っている。 ida -> aの関数なので、