Home /

monad

haskellのmonadを説明する

これは最近学んだことについて説明(統合)を試みる記事です。例のごとくボトムアップで解説したほうが分かりやすいので算数から上がっていくことにする。以降のステップは動機によって推進するため、必然的に積み重なっていくものではないと思う(少なくとも私の認識において)。

1.値を計算したい

inc :: Int -> Int
inc = (+1)

> inc 2
3

inc:: Int -> IntはInt型の値に1 :: Intを加算する関数とします。上の例は2に1を足しているだけで、何か深い意味があるわけではない。inc 5なら6になる。構造を持つ初等教育レベルの計算です。

incは上記で定義したもの以外の挙動をしない。 以降の関数も示された実装が全てなので不可視な仕様を考察しなくてよい。

Image from Gyazo

2.Functor|文脈に包まれた値を計算したい

次の場合について考える

> inc (Cxt 2)
error: [GHC-83865]

Cxt 2という表記を見たことがないならピンとこないかもしれない。例えばリストという文脈に包まれた2と解釈していいし、何か文脈(Context)を持っている2 :: Intということだ。

今記事では説明のために、文脈で包むだけの最小のCxt型を導入する。

data Cxt a = Cxt a
  deriving Show

話を戻す。

Cxt 2incに適用すると型エラーになる。 理由はincがInt型以外の引数への適用を定義していないからだ ここで重要なのはCxt 3 :: Cxt Int3 :: Intはhaskellによって明確に区別されているという点です。文脈の中にInt型があるからといって即座に計算できるわけではありません。

ここで動機が生まれます。

文脈が包む値を包まなかった頃のように計算したい

inc 23なのだからinc (Cxt 2)Cxt 3を返すのは明白だ」と思いませんか?

この動機を実現するために愚直に考えれば、Cxt Int型の関数を個別に実装すれば当然に解決する。型エラーになるのは引数にCxt Int型を取る実装を定義していないからだ。以下に例を示す。

incCxt :: Cxt Int -> Cxt Int
incCxt (Cxt n) = Cxt (inc n)

> incCxt (Cxt 2)
Cxt 3

ここでincCxtの内部でCxt Int型を定義していないincの計算が成立している点に注目してほしい。incで引数にCxt Int型を取るとエラーだったのに対して、Int内部の計算ではInt型を引数に受け取ってエラーにならない。

両者のブレイクポイントは「パターンマッチの有無」です。 上記の例の観察からパターンマッチで値から文脈を剥がせることが分かります。より明確な例として、Cxt Intを受け取って文脈を剥がし、Int型を返す関数cxtToint :: Cxt Int -> Intを示します。

cxtToint :: Cxt Int -> Int
cxtToint (Cxt n) = n

> cxtToint (Cxt 42)
42 :: Int

haskellでは型構築子は関数と同じように扱えるので、intCxtではパターンマッチでCxt nCxtnに分解し、nに1を加算してCxtの引数に渡しています。

> (inc 2)
3 :: Int

> Cxt (inc 2)
Cxt 3 :: Cxt Int

> cxttoint (Cxt (inc 2))
3 :: Int
  • 文脈を剥がす(パターンマッチ)
  • 文脈で包む(型構築子に値を適用する)

これらができると分かれば、計算する場合は文脈を剥がし、返す値を文脈で包む計算を抽象化(λ抽象化して名前をつける)することで文脈を維持したまま計算してるように振る舞うことができます。そして計算は(+1)以外でも可能です。

> incCxt (Cxt 2)
Cxt 3

double :: Int -> Int
double n = (*2) n

double 2
> 4

doubleCxt :: Cxt Int -> Cxt Int
doubleCxt (Cxt n) = Cxt (double n)

> doubleCxt (Cxt 2)
Cxt 4

計算内容は適切であるならばなんでもいいので抽象化します。 あとInt型以外も扱えるようにIntを一般化してa,bとします。

a,bに代入された型に沿って型が決定します。代入される型はHask圏(Haskellで使える型)であるならばなんでもいいです。 例えばa = Int,b = Stringならばf :: a -> bf :: Int -> Stringとなる。

mapCxt :: (a -> b) -> Cxt a -> Cxt b
mapCxt f (Cxt n) = Cxt (f n) 

mapCxt inc (Cxt 2)
> Cxt 3

{-
a = Int
b = Int
-}

mapCxt double (Cxt 2)
> Cxt 4

mapCxtの抽象化によって、文脈で包まれた値を引数に取っても、Int型の関数で計算し、Cxt Int型を返すことができることが分かった。

mapCxtはCxtだけの例だったが、身近な例としてリストの計算を示す。

{-
data List a = [] | a : List a

map _ []     = []
map f (x:xs) = f x : map f xs
-}

map inc [2]
[3]

> map inc [1..5]
[2,3,4,5,6]

map :: (a -> b) -> [a] -> [b] ではCxtの代わりにListが文脈となっているだけで、文脈で包まれた値を受け取って、値の計算をして、文脈を包んで返すという計算は同じなのは分かる。

複数の値を持つ[1..5]が計算できているのは、そう定義されているだけにすぎない。文脈に包まれた値を計算するという軸はブレいていないので気にしなくていい。

Image from Gyazo

mapが文脈で包まれた値を扱う場合は大体こういう挙動になります。 またListは糖衣構文で理解を妨げるので、Maybe型も示しておきます。

mapMaybe :: (a -> b) -> Maybe a -> Maybe b 
mapMaybe _ Nothing   = Nothing
mapMaybe f (Just x)  = Just (f x)

> mapMaybe inc Nothing
Nothing

> mapMaybe inc (Just 2)
just 3

型クラスの導入

次にここまで個別に実装してきた関数を見てみます。

mapCxt   :: (a -> b) -> Cxt a   -> Cxt b
map      :: (a -> b) -> [a]     -> [b]
mapMaybe :: (a -> b) -> Maybe a -> Maybe b

どれも型シグネチャの形が (a -> b) -> f a -> f b という同一構造である。haskellでは型クラスでまとめることができる。型クラスは個別に実装していた同じ構造を持つ関数を同じ関数でまとめることができるので嬉しい。

Image from Gyazo

これを次のようにできる。

Image from Gyazo

lispのような動的型付けの言語だと実行中に型で条件分岐できる(なので実行中に型エラーになる)。haskellは静的型付けなので、実行前に型エラーを検出する必要がある(実行中に型エラーを出さない)。入力された型を区別したい時、実行前に条件分岐しないといけない。型クラスはその需要を満たし、インスタンスが。

自作した型は個別にインスタンスを作る必要がある。

class Functor f where
  fmap :: (a -> b) -> f a -> f b

instance Functor Cxt where
  fmap f (Cxt x) = Cxt (f x)

型クラスとインスタンスの実装を統合すると個別に実装したように見做せる。これは擬似コードだ。

fmap :: (a -> b) -> f a -> f b -- type class
fmap f (Cxt x) = Cxt (f x)     -- instance

Code

data Cxt a = Cxt a
  deriving Show

inc :: Int -> Int
inc n = (+1) n

incCxt :: Cxt Int -> Cxt Int
incCxt (Cxt n) = Cxt (inc n)

cxtToint :: Cxt Int -> Int
cxtToint (Cxt n) = n

double :: Int -> Int
double n = (*2) n

doubleCxt :: Cxt Int -> Cxt Int
doubleCxt (Cxt n) = Cxt ((*2) n)

mapCxt :: (Int -> Int) -> Cxt Int -> Cxt Int
mapCxt f (Cxt n) = Cxt (f n)

mapMaybe _ Nothing   = Nothing
mapMaybe f (Just x)  = Just (f x)
a monad is a monoid in the category of endofunctors, what's the problem?