bitterharvest’s diary

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

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

10.3 プログラムの結合

前々回の記事で紹介した二つのプログラムを決後するフィッシュ・オペレータをHaskellで実現することを考えよう。二つのプログラムを結合するとは、下図を実現することである。
f:id:bitterharvest:20170805091209p:plain

上図では二つのプログラム\(f,g\)がある。それぞれのプログラムは\(a,b\)を入力する。これらの値はコンピュータの世界で通用するものだ。そして、これらのプログラムは\(m \ b, m \ c\)を出力する。これらの値は人間の世界で通用するものだ。最初のプログラム\(f\)が吐き出す出力\(m \ b\)は、次のプログラム\(g\)への入力\(b\)となっているが、出力を直接入力として渡すわけにはいかない。人間の世界からコンピュータの世界への変換を行わないといけない。さて、これをHaskellで実現することを考える。

このように言われても戸惑うだろう。そこで、入出力が直接つながっている関数の合成\((.)\)をHaskellで実現し、そこからどのように実現したらよいかを学び取ることにしよう。

関数の合成は二つの関数を得て、それを合成し一つの関数として返すことである。Hasekllで実現すると次のようになる。最初の行が型シグネチャである。

(.) :: (b -> c) -> (a -> b) -> (a -> c)
g . f = \a -> let b = f a
              in g b

上記のプログラムで、\((b->c),(a->b)\)が入力される関数\(g,f\)である。\((a->c)\)が出力される関数で\(g \circ f\)である。

二番目の行が関数\((.)\)の定義である。\(a\)を入力し、それを関数\(f\)に適用し、その出力が\(b\)である。そして、この値を関数\(f\)に適用し、その出力が\(C\)である。

これをProgramというモジュールの一部とする。モジュールは次のようになる。

module Program ((Program..))) where

(.) :: (b -> c) -> (a -> b) -> (a -> c)
g . f = \a -> let b = f a
              in g b

これを用いてみよう。合成関数\((.)\)はHaskellでは最初から定義され与えられているので、元々あったものと区別するために、\((P..)\)で呼ぶようにし、次のようにメインモジュールを作成する。

import Program as P

f a = a + 2
g a = a * 3
h = g P.. f

上のプログラムで\(h\)が合成された関数である。これを用いてみよう。

Prelude> :load "Main.hs"
[1 of 2] Compiling Program          ( Program.hs, interpreted )
[2 of 2] Compiling Main             ( Main.hs, interpreted )
Ok, modules loaded: Main, Program.
*Main> h 5
21
*Main> h (-3)
-3

期待する結果は得られているようである。それでは、関数の合成のコードを見ながらプログラムの合成についてのコードを作成することにしよう。
変えなければならないところは、最後の行の\( g b\)である。ここは、出力\(b\)を得て、\(g\)に適応している。そこで同じようにしよう。最初のプログラムの出力は\(mb\)としよう。これは型\(m \ b\)に属す値という意味でこのようにした。また、これを受けて\(g\)に適応するのは\(mb >>= g\)としよう。

即ち、次のようにする。

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

関数合成のコードは次のようになっていた。これと比較すると対応関係がよく分かる。

(.) :: (b -> c) -> (a -> b) -> (a -> c)
g . f = \a -> let b = f a
              in g b

前々回の記事で紹介したが、クライスリ圏は、\(>=>\)と\(return\)を定義すれば構成できる。\(>=>\)はいま出てきた\(>>=\)から得られるので、クライスリ圏は、\(>>=\)と\(return\)からも構成できる。そこで、この二つの関数を備えているクラスをモナドと呼ぶことにしよう。このように定めるとモナドは次のようになる。

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

上記で、\(>>=\)は人間の世界からの入力\(m \ a\)、人間の世界への出力\(m \ b\)としている。しかし、ここで用いられる関数\(a \rightarrow m \ b\)は\(m \ a\)をコンピュータの世界の値\(a\)に変換されたものを用いて計算している。

今、人間の世界に移す時は[ ]で値を囲むことにする。そこで、[ ] をモナドとすることを考えよう。これは、次のように定義できる。

class Monad m where
instance Monad [] where
  [a] >>= g = g a
  return a = [a] 

そこで、今までのものをProgramというモジュールにまとめる。この時、Haskellで使われている関数との衝突を避けるために、これらには\(Program.\)をつける。

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

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

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

instance Program.Monad [] where
  [a] >>= g = g a
  return a = [a] 

それではこのプログラムを使ってみよう。利用するプログラムは以下のとおりである。

import Program as P
f' a = [a + 2]
g' a = [a * 3]
h' = f' >=> g' 

先ほどと変わらないが、出力がカッコで囲まれていることに注意してほしい。

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

これで、プログラムの結合ができるようになった。次回は、集合と冪集合での関係を用いてモナドを定義する。