Haskell
モナド

だんだんわかる モナド入門 (Haskell)

More than 1 year has passed since last update.


概要

Haskellといえばモナドです。

自分がHaskellでモナドについて勉強してわかったことを、

理解を深めるために整理しつつ書いていきたいと思います。

他の解説では、Maybeモナドなど例から学ぶパターンが多いですが

ここではまず簡単に理論から入っていきます。


前提

Haskellの基本(関数、高階関数、部分適用などなど)について特別書いたりはしません。


0. モナドとは一体...?

名前からは実態が掴みづらい「モナド」ですが、それほど難しいものではありません。

モナドとは箱のようなものです。

値を包んだものがモナドです。

MyMonad というモナドを作ってみます。

data MyMonad a = MyMonad a

MyMonad "Hello!" :: MyMonad String
MyMonad 3 :: MyMonad Int
MyMonad True :: MyMonad Bool

・・・。

しかしこれだけではモナドとは呼べません。

それではこれをモナドに育てていきましょう。


1. Functor (関手)

まずは Functor です。

箱に入った値に関数を適用できるようにします。

関手は数学の圏論という分野の概念ですが、ここで圏論について知る必要はありません。

先ほどの MyMonad を Functor に成長させましょう。

instance Functor MyMonad

この Functor というのはHaskellの型クラスです。

class Functor f where

fmap :: (a -> b) -> f a -> f b

fmap という関数が出てきました。これが関手です。

MyMonadの実装はこうです。

instance Functor MyMonad where

fmap f (MyMonad a) = MyMonad (f a)

箱から値を取り出し、その値に関数を適用した結果をまた箱に入れています。

リストの map 関数を考えるとわかりやすいと思います。

f :: Int -> Int

f x = x * 2

map f [1, 2, 3] --> [2, 4, 6]

関手はこれだけです。

ただしルールがあります。

fmap id == id

fmap (f . g) == fmap f . fmap g

一つ目は当たり前です。

二つ目は複雑そうですが、合成してからfmapに適用しても、適用してから合成しても結果は同じということです。分配法則のようですね。


Functor に進化した!

これで、箱に値が入ったまま関数を適用できるようになりました。

ここまでくればモナドの半分はできたようなものです。


2.Applicative

お次は Applicative です。名前は長いですが、Functor がパワーアップしただけです。

Applicative も型クラスです。

class Functor f => Applicative f where

pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

Applicative のインスタンスになるには Functor のインスタンスでなくてはなりません。

核となる二つのメソッドを紹介します。

pure 関数は簡単で、値を箱に入れます。

これを 持ち上げ と言います。

MyMonad の場合はこうですね。

instance Applicative MyMonad where

pure a = MyMonad a

さて、肝心なのは <*> 演算子です。

先ほどの fmap に似ていますね。

fmap :: (a -> b) -> f a -> f b

(<*>) :: f (a -> b) -> f a -> f b

違うのは、<*> 演算子の方は引数に取る関数も箱に入っているということです。

MyMonad の実装はこうです。

instance Applicative MyMonad where

(MyMonad f) <*> (MyMonad a) = MyMonad (f a)

箱に入った関数と値を受け取って、値に関数を適用した結果を箱に入れて返しています。


Applicative に進化した!!

箱に入った関数を適用できるようになりました。

こんなこともできます。

(*) <$> MyMonad 2 <*> MyMonad 5   --> MyMonad 10

<$> 演算子は fmap 関数と同じ意味です。

まず Functor の fmap が適用されて

(*) <$> MyMonad 2   --> MyMonad (2 *)

となり、Applicative の <*> が適用されて

MyMonad (2 *) <*> MyMonad 5   --> MyMonad (2 * 5) => MyMonad 10

となるわけです。

これはいくらでもつなげることができます。


3.Monad

ついに Monad にたどり着きました!

Monad は Applicative のパワーアップ版です。

class Applicative m => Monad m where

return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

return は Applicative の pure と同じで、値をモナドに包んで返します。

(>>=) 演算子は bind とも言い、Monadの肝です。

実装してみましょう。

instance Monad MyMonad where

return = pure
(MyMonad a) >>= f = f a

(>>=) 演算子は、第二引数に 値を取りモナドを返す関数 を取ります。

第一引数のモナドから値を取り出し、それを関数に注ぎ込んでいるイメージです。


モナドに進化した!

(>>=) 演算子を使うと、モナドをいくつもつなげていくことができます。

f :: Int -> MyMonad Int

f x = return $ x + 2

g :: Int -> MyMonad Int
g x = return $ x * 2

MyMonad 1 >>= f >>= g --> MyMonad 6
MyMonad 3 >>= g >>= f >>= g --> MyMonad 16


その他の演算子

Applicative や Monad には便利な演算子が他にもあります。

これらは全て上で紹介した基本となる関数や演算子によって定義されています。

実装を見てみましょう。


Applicative

class Functor f => Applicative f where

(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a

a *> b = pure (const id) <*> a <*> b
a <* b = pure const <*> a <*> b

(*>) 演算子は左辺を捨てて右辺のみを返します。

(<*) 演算子は右辺を捨てて左辺のみを返します。

左結合です。

MyMonad "ABC" <* MyMonad "DEF"        --> MyMonad "ABC"

MyMonad 1 *> MyMonad 2 <* MyMonad 3 --> MyMonad 2


Monad

class Applicative m => Monad m where

(>>) :: m a -> m b -> m b
fail :: String -> m a

(>>) = (*>)
fail s = MyMonad s

(>>) 演算子は、前の値を捨てて後の値を返します。

Applicative の (*>) 演算子と同じ働きです。

fail 関数はほぼ不要でしょう。

(後述する do構文内で、パターンマッチが失敗した時に使われます。)


do構文

do構文を使うと、複数の bind演算をすっきり書くことができます。

逐次実行:

do  MyMonad 1       

MyMonad 2
MyMonad 3
...

MyMonad 1 >> MyMonad 2 >> MyMonad 3 ...

値の取り出し(パターンマッチ):

do  x <- MyMonad 2     

MyMonad x

MyMonad 2 >>= (\x -> MyMonad x)

let式(局所変数):

do  let x = 3

MyMonad x

let x = 3 in MyMonad x


まとめ


モナドと手続き型プログラミング

モナドを使うと、関数型のHaskellの世界で手続き型プログラミングの手法が使えるのです。

手続き型プログラミングでは3つの構造を使いますね。

つまり、逐次・反復・分岐 です。

反復については、関数型プログラミングでは再帰呼び出しで表現します。

逐次実行を可能にしているのが Applicative です。

そして Monad を使うと、分岐が可能になります。

Monad は、前の結果を利用して後の結果を変えることができます。

ここが Applicative との大きな違いです。


最後に

分かりにくかったかもしれませんが、モナドについて自分なりに整理してまとめてみました。

理解が間違っているところがあったら、指摘していただけると幸いです。

具体的なモナドについては書けませんでしたが、

今後 MonadPlus などについて追記していくつもりです。