bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

Haskellの難関モナドを突破するために掘り下げて学ぶ(4)

10.4 モナドを別の方法で定義する

前の記事でモナドを定義するときに\(>>=\)という関数を用いた。この関数の型シグネチャは次のようになっていた。

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

ここで、\(m\)を関手と考えてみよう。関手\(F\)は下図のように定義されていた。
f:id:bitterharvest:20170806100853p:plain

上図で、関手\(F\)によって、対象\(A,B\)は対象\(F(A),F(B)\)に、また射\(f\)は射\(F(f)\)に写像される。

これを\(m\)を関手と考えてHaskellの記述で上記の図を書き直すと次の図のようになる。
f:id:bitterharvest:20170806101029p:plain

上図で、関手\(m\)によって、値\(a,b\)は値\(ma,mb\)に、また射\(f\)は射\(fmap \ f\)に写像される。少しわかりにくいのだが、\(a,b\)は型変数\(a,b\)がある型を取った時の値である。同様に、\(ma,mb\)は型変数\(m \ a,m \ b\)がある型を取った時の値である。また、Haskellでは\(F(f)\)は\(fmap \ f\)で表わす。

\( (>>=)\)のところで出てきた関数は\( (m \ a \rightarrow (a \rightarrow m \ b) \rightarrow m \ b)\)は、関手\(F\)による射\(f\)の写像\(F(f)\)を表していると考えよう。そうすると、\( m \ a\)が\(F (A)\)に、\(a \rightarrow m \ b\)が\(f\)に、\( m \ b\)が\(F (B)\)に対応している。そして、\(ma >>= f = fmap \ f \ ma\)となる。これから、\(b\)が\(mb\)であることが分かるので、上の図を書き直すと下図を得る。
f:id:bitterharvest:20170806102415p:plain

ところで、\( (>>=)\)の型シグネチャを見るとその出力は\(m \ b\)となっている。しかし、上の図での出力の型シグネチャは\(m \ (m \ b)\)である。このため、この矛盾を解決するためには、下図に示すように\(m b\)と\(m (m b)\)は同じでなければならない。
f:id:bitterharvest:20170806105932p:plain

\(m\)はコンピュータの世界から人間の世界へと視点となる世界を代えていた。丁度包み紙で包み込むような性質なのでこのようなものはコンテナと呼ばれる。\(m\)を二回かぶせた場合には人間の世界から人間の世界へと変えているので何も変わらない。即ち、コンテナを二回被せたものは一回被せたものと同じである。\(m \ (m \ b)=m \ b\)である。そこで、この性質を関数にし\(join\)と名付けよう。即ち、\(join :: m \ (m \ b) \rightarrow m \ b\)。このようにすると\(>>=\)は次のように定義できる。

(>>=) :: m a -> (a -> m b) -> m b
ma >>= f = join (fmap f ma)

モナドを\(>>=\)と\(return\)のペアではなく、\(join\)と\(return\)のペアで定義すると次のようになる。

class Functor m => Monad m where
  join :: m (m a) -> m a
  return :: a -> m a

前回と同じようにコンピュータの世界での表現に[ ]をつけたものを人間の世界での表現ということにすると[ ]をモナドとして定義できる。
これらをモジュールとして記述したのが下記のコードである。

module Program1 (Program1.Monad, (>=>)) where

(>=>) :: (Program1.Monad m) => ( a -> m b) -> ( b -> m c) -> (a -> m c)
f >=> g = \a -> let mb = f a
                in mb Program1.>>= g

(>>=) :: (Program1.Monad m) => m a -> (a -> m b) -> m b
ma >>= f = join (fmap f ma)

class Functor m => Monad m where
  join :: m (m a) -> m a
  return :: a -> m a

instance Program1.Monad [] where
  join [[a]] = [a]
  return a = [a] 

前回と同様にこれを利用するために下記のプログラムを用意する。

import Program1 as P

f' a = [a + 2]
g' a = [a * 3]
h' = f' >=> g'

前回と同じように実行すると次の結果を得る。

Prelude> :load "Main1.hs"
[1 of 2] Compiling Program1         ( Program1.hs, interpreted )
[2 of 2] Compiling Main             ( Main1.hs, interpreted )
Ok, modules loaded: Main, Program1.
*Main> f' 5
[7]
*Main> g' 7
[21]
*Main> h' 5
[21]
*Main> h' (-3)
[-3]

ところで、\(join\)は前々回の集合と冪集合を説明するときに出てきたものだ。冪集合から冪集合を作ったとしても元の冪集合と同じであるという話をしたが、これに相当するのが、\(join\)である。従って、今回のモナドの定義は集合と冪集合の関係を定義したものということができる。その時に用いた図を再掲する。
f:id:bitterharvest:20170806111003p:plain

モナドの理解が進んだところで、モナドの使い方を次回は学ぶ。

最後にモナドについてまとめておこう。

モナドとは、コンテナと呼ばれる性質を持つ関手である。関手は一方の世界(コンピュータの世界)の構造を他方の世界(人間の世界)に写すことができる。コンテナは一方の世界を他方の世界で包む性質である。

コンテナには二つの機能がある。一つの機能は一、方の世界のものを他方の世界で表現したものに写すものである。これは恒等射の性質を拡張したものである。二つ目の機能は関手を二回適応したものは関手を一回適応したものと同じであるというものだ。関手は一方の世界を他方の世界に写すだけでなく、他方の世界を他方の世界へ写すこともできる。後者は他方の世界で二重にくるむことになるが、これは一重でくるんだものと変わらないというのが二番目の機能である。

コンテナの二つの機能は\(return\)と\(join\)と呼ばれる二つの関数で実現される。