top page > computer > haskell > web_lecture > for_programmer > monad_log.html
更新日:
文責: 重城良国

モナド: 計算のログ

(工事中 60%)

monad_log.hs

はじめに

ここまででモナドの基本は学んだ。モナドはなかみではなく形式だ。

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

のふたつの関数があり

return x >>= f == f x
m >>= return == m
(m >>= f) >>= g == m >>= (\x -> f x >>= g)

を満たせば内容に関係なくそれはモナドだ。(a -> m b)型の関数と(b -> m c)型の関数とをつなぐことができ、つなぎかたが左結合でも右結合でも結果は同じで、左右どちらからつないでも関数を変化させない特別な関数returnがあるということだ。モナドのもうひとつの例として計算のログをとる仕組みを作る。

ログを残す関数の例

型Logger

ログを残しながら文字コードを求める関数を考える。

toCode :: Char -> Logger Int

たとえばtoCode 'c'はログとして["toCode 'c'"]を持ち、計算の結果として99を持つ。つまり、Logger型の値は文字列のリストと結果の値を持つ。Logger型の定義は

data Logger a = Logger [String] a deriving Show

のようになる。

関数toCode

関数toCodeの定義は

toCode c = Logger ["toCode " ++ show c] (ord c)

となる。

関数double

同様にログを残しながら整数値を2倍する関数は

double :: Int -> Logger Int
double n = Logger ["double " ++ show n] (n * 2)

となる。

ファンクター、アプリカティブファンクター

モナドであれば型クラスFunctor, Applicativeのインスタンス宣言は機械的にできる。さきに書いてしまおう。

instance Functor Logger where
fmap = (=<<) . (return .)

instance Applicative Logger where
pure = return
mf <*> mx = [ f x | f <- mf, x <- mx ]

ソースファイルの先頭に

{-# LANGUAGE MonadComprehensions #-}

を書いておく。

モナド

関数returnはログを残さずに値を返せばいいだろう。ログを返す関数をつなぐにはどうしたらいいだろうか。

g :: a -> Logger b
h :: b -> Logger c

であるような関数gとhをつなぐことを考える。まずgに引数xをあたえる。返り値にはログlとb型の値yとが含まれているはずだ。値yを関数hにあたえる。すると返り値にはログl'と値zとが含まれている。最終的な結果にはログ(l ++ l')と値zを含めればいい。

(g >=> h) x = let
Logger l y = g x
Logger l' z = h y in
Logger (l ++ l') z

演算子(>>=)の型に合わせて変形すると

Logger l y >>= h = let
Logger l' z = h y in
Logger (l ++ l') z

型クラスMonadのインスタンス宣言は

instance Monad Logger where
return = Logger []
Logger l x >>= f = let Logger l' y = f x in Logger (l ++ l') y

となる。

関数toCodeDouble

ログを残しながら文字コードを2倍する関数は

toCodeDouble :: Char -> Logger Int
toCodeDouble c = toCode c >>= double

となる。

% ghci monad_log.hs
*Main> toCodeDouble 'c'
Logger ["toCodeDouble 'c'","double 99"] 198

関数tell

関数tellを定義すると関数toCodeやdoubleをより抽象化することができる。

tell :: String -> Logger ()
tell l = Logger [l] ()

toCode :: Char -> Logger Int
toCode c = tell ("toCode " ++ show c) >> ord c

double :: Int -> Logger Int
double n = tell ("double " ++ show n) >> (n * 2)

関数toCodeやdoubleが型Loggerのなかみについて知っている必要がなくなる。関数tellとモナドインターフェースだけで定義されている。

まとめ

モナドのさらなる例としてログを残しながら演算をする仕組みを作った。なかみには関係なくモナド関数をもち、それらがモナド則を満たしさえすれば、それはモナドだ。裸の値を文脈にいれる関数をつなぐことができ、そのつなぎかたが単位元をもち、結合則を満たすということだ。

「モナド則からモノイド則を導く」へもどる 「モナド: ライオンの檻」へ


メモ

書籍ではこれを演習問題として使おうかな。演習問題ではできれば「問いかけ」「答え」のようにしたい。そしてできるだけ「問いかけ」と「答え」のページをかえたい。ページxに「問いかけ1」「問いかけ2」「問いかけ3」「問いかけ4」があってページx+1に「答え1」「答え2」「答え3」「答え4」がある感じか。ただたとえば「問いかけk+1」が「答えk」に依存するときにどうするかが問題だ。そこらへんはうまくごまかせるだろうか?また「自力で解くまで」先に進めない人がそこで読むのをやめてしまう問題を解決したい。

今はとりあえずスピーディーに終わらせるために変更はしないが関数tellの名前はlogのほうがいいかもしれない。

「モナド則からモノイド則を導く」へもどる 「モナド: ライオンの檻」へ

正当なCSSです! HTML5 Powered with CSS3 / styling, and Semantics