読者です 読者をやめる 読者になる 読者になる

bitterharvest’s diary

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

HaskellドリルⅥ モナドのdo記法

1.モナドの復習

圏論の中でモナドは次のように定義されていた。

モナドは関手\(\mathcal{M}:\mathcal{C} \rightarrow \mathcal{C}\)で、\(\mathcal{C}\)内のすべての対象\(X\)に対して、次の二つの射を有する。
1)\(unit_{X}:X \rightarrow \mathcal{M} (X)\)
2)\(join_{X}:\mathcal{M}(\mathcal{M}(X)) \rightarrow \mathcal{M} (X)\)

簡単に言うと、モナドは\(unit\)と\(join\)という性質を持つ自身の圏への関手である。

Haskellでも同じようにreturnと(>>=)という関数を用いてモナドを定義していた。そして、これと圏論でのモナドは等価であることも分かった。そこで、Haskellモナドの定義を調べると次のようになっている。

Prelude> :info Monad
class Monad (m :: * -> *) where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a
  fail :: String -> m a
  	-- Defined in ‘GHC.Base’
instance Monad Maybe -- Defined in ‘Data.Maybe’
instance Monad (Either e) -- Defined in ・eData.Either’
instance Monad [] -- Defined in ‘GHC.Base’
instance Monad IO -- Defined in ‘GHC.Base’
instance Monad ((->) r) -- Defined in ‘GHC.Base’

returnと(>>=)の他に、(>>)とfailという関数が用意されてる。(>>)という関数は、m a >>= \_ -> m bの糖衣構文である。即ち、入力として受け取るaの値にかかわらず、m bを出力する。また、failはエラーが生じたときに使われる関数である。

さらに、モナドインスタンスとして、Maybe, (Either e), リストがインスタンスとして用意されている。また、まだ説明していないが、IOもインスタンスである。さらに、ラムダ記法もモナドインスタンスであることが分かる。

2.(>>=)の連結

ところで、(>>=)と(>>)はモナドを入力として受け取り、モナドを出力とするので、(>>=)を連結したり、(>>)を連結したり、(>>=)と(>>)を連結したりすることができる。

例えば、変数xを4にし、変数yをxに5を足したものとし、変数yの二乗を求めるときは次のようにしていた。

Prelude> let x = 4; y =x + 5 in y * y
81

これをモナドのMaybeで行うとしたら次のようになる。

Prelude> Just 4 >>= \x -> Just (x + 5) >>= \y -> Just (y * y)
Just 81

今の例では、最後の計算はyだけで行われていたが、最後の計算にxを加えるようにすると、最初の例は次のようになる。

Prelude> let x = 4; y =x + 5 in y * y + x
85

また、モナドのMaybeを使ったほうは次のようになる。

Prelude> Just 4 >>= \x -> Just (x + 5) >>= \y -> Just (y * y + x)
Just 85

変数の適応範囲をはっきりさせるためにカッコをつけると次のようになる。

Just "Good" >>= (\x ->  Just " " >>= (\ y -> Just "Luck" >>= (\z -> Just "!" >>= (\w -> Just  (x ++ y ++ z ++ w)))))

もう少し例を挙げる。変数xを文字列"Good"とし、変数yを文字列" "とし、変数zを文字列"Luck"とした、変数wを文字列"!"とした時、これらをつなぎ合わせた文字列は次のようになる。

Prelude> let x = "Good"; y = " "; z = "Luck"; w ="!" in x ++ y ++ z ++ w
"Good Luck!"

Maybeを用いると次のようになる。

Prelude> Just "Good" >>= \x ->  Just " " >>= \ y -> Just "Luck" >>= \z -> Just "!" >>= \w -> Just  (x ++ y ++ z ++ w)
Just "Good Luck!"

変数の適応範囲をはっきりさせるためにカッコをつけると次のようになる。

Prelude> Just "Good" >>= \x ->  Just " " >>= \ y -> Just "Luck" >>= \z -> Just "!" >>= \w -> Just  (x ++ y ++ z ++ w)
Just "Good Luck!"


上の例で、どこかに、Nothingが入っていると次のようになる。たとえば、return " "のところが、Nothingだとすると、

Prelude> Just "Good" >>= \x ->  Nothing >>= \ y -> Just "Luck" >>= \z -> Just "!" >>= \w -> Just  (x ++ y ++ z ++ w)
Nothing

これは他の場所でも同じで、Nothingがどこかにあると、Nothingが変える。

ところで、モナドでの記述を一行に一つの処理が入るようにして次のように書き換えてみる。

最初の例は関数foo1にして、次のようする。

foo1 :: Maybe Integer
foo1 = Just 4 >>= (\x ->
       Just (x + 5) >>= (\y ->
       Just (y * y + x)))

2番目の例は関数foo2として次のようにする。

foo2 :: Maybe Integer
foo2 = Just "Good" >>= (\x ->
       Just " " >>= (\ y ->
       Just "Luck" >>= (\z ->
       Just "!" >>= (\w ->
       Just  (x ++ y ++ z ++ w)))))

上記の表現は分かりにくいので、これを見やすくしたのがdo記法である。do記法では処理と変数の順番をさかさまにして次のように書く。

最初の例は次のようになる。

foo1 :: Maybe Integer
foo1 = do
  x <- Just 4
  y <- Just (x + 5)
  Just (y * y + x)

二番目の例は次のようになる。

foo2 :: Maybe String
foo2 = do
  x <- Just "Good"
  y <- Just " " 
  z <- Just "Luck"
  w <- Just "!"
  Just  (x ++ y ++ z ++ w)

(>>)を使用した例を挙げておく。"Oh!"の後にブランクがあるのだがそれを無視する場合は(>>)を使い、次のようになる。

Prelude> Just "Oh" >>= \x -> Just "!" >>= \y -> Just " " >> Just (x ++ y)
Just "Oh!"

これは、(>>)を(>>=)で置き換えれば、次と等価である。

Prelude> Just "Oh" >>= \x -> Just "!" >>= \y -> Just " " >>= \_ -> Just (x ++ y)
Just "Oh!"

これをdo記法で書くと次のようになる。

foo3 :: Maybe String
foo3 = do
  x <- Just "Oh"
  y <- Just "!"
  _ <- Just " "
  Just (x ++ y)
Just "Oh!"

このようにdo記法で記述すると、モナドでの処理はおなじみの逐次型プログラミングと似てくる。

3.問題

それでは問題を解いて理解を深めることにする。

問題1:変数xに5を束縛し、変数yに6を束縛しその積を求めなさい。

問題2:上記プログラムをモナドMaybeで書きなさい。

問題3:上記プログラムを関数foo4としてdo記法で書きなさい。

問題4:変数xに3.14を束縛し、変数yに5を束縛し、2*x:yを求めなさい。さらに、これをモナドMaybeで書いた後、関数foo5としてdo記法で書きなさい。

問題5:変数xに"Yes, ", 変数yに"I ",変数zに"do."を束縛し、これらを順番につなげた文字列を出力するようにしなさい。

問題6:上記プログラムをモナドMaybeで書きなさい。

問題7:上記プログラムを関数foo6としてdo記法で書きなさい。

問題8:変数xに[1,3], 変数yに[5,7],変数zに[9,11]を束縛し、これらを順番につなげたリストを出力するようにしなさい。

問題9:上記プログラムをモナドMaybeで書きなさい。

問題10:上記プログラムを関数foo7としてdo記法で書きなさい。