今回は Writer モナドと bind について使い方を調べます。
Writer モナドは複雑なので、Writerで実現したい計算に近いコードを Maybe モナドで書き、その後それを Writer モナドに書き換えます。
Maybe と bind を使う。
$ ghci
> (Just 10) >>= (\x -> Just $ x * 2) >>= (\x -> Just $ x * x)
Just 400
計算の途中に Nothing になる関数があっても問題ない、を確かめる。
$ ghci
> (Just 10) >>= (\x -> Just $ x * 2) >>= (\x-> Nothing) >>= (\x -> Just $ x * x)
Nothing
ラムダ関数を使っていると分かり辛い。名前付き関数に変更する。
maybe.hs:
twice :: Int -> Maybe Int
twice x = Just $ x * 2
square :: Int -> Maybe Int
square x = Just $ x * x
mynothing :: Int -> Maybe Int
mynothing _ = Nothing
GHCi で確認:
$ ghci
> :load maybe.hs
> (Just 10) >>= twice >>= square
Just 400
> (Just 10) >>= twice >>= mynothing >>= square
Nothing
いよいよ、本題の Writer とその bind を使う。
型が Maybe Int の値をつくるには、たとえば Just 10 としていた。
$ ghci
> :t Just 10
Just 10 :: Num a => Maybe a
> :t Just 10 :: Maybe Int
Just 10 :: Maybe Int :: Maybe Int
一方、Writer String Int 型の場合、その値は以下のようにしてつくる:
> import Control.Monad.Writer
> writer (10, "Hello") :: Writer String Int
WriterT (Identity (10,"Hello"))
型変数の順番が writer 関数に適用するタプルは逆になる. 主たる計算 Int が 先で、おまけ String 後 と考えれば自然とも言える。
ここでは 主が Int 型で おまけが String 型になる。 writer 関数の型シグネチャを確認:
> :t writer
writer :: MonadWriter w m => (a, w) -> m a
もし、おまけのString が空文字列でよければ、次のようにつくることもできる。
> (pure 10 :: Writer String Int)
WriterT (Identity (10,""))
これは、writer 関数で writer (10, "") :: Writer String Int したのと同じことになる。
では bind してみる。
> (pure 10 :: Writer String Int) >>= (\x -> writer (x * 2, "twice ")) >>= (\x -> writer (x * x, "and square"))
WriterT (Identity (400,"twice and square"))
主たる計算は普通に行われ、その上でさらにおまけの文字列部分が蓄積されていく。(処理内容のログの書き出しのイメージ)
今度は、おまけ部分の型を String から [String] に変更する:
> (pure 10 :: Writer [String] Int) >>= (\x -> writer (x * 2, ["twice"])) >>= (\x -> writer (x * x, ["square"]))
WriterT (Identity (400,["twice","square"]))
先ほどはログをいい感じの文字列にするため、おまけの文字列を twice と and square などと細工していた。 今度は、おまけ部分を 文字列のリスト 型に変更したことで、そのような細工は不要になった。 ただし最終的な値は、["twice","square"] になる。 これをログとしてわかりやすい文字列に変換する関数は別途必要。
こんな感じだろうか:
aPrettyLogMessage = foldl1 (\acc item -> acc ++ " and " ++ item) ["twice","square"] -- "twice and square" になる.
runWriter という便利な関数を使うと、 たとえば、型が Writer [String] Int だった場合、その中身を (Int, [String]) のタプルとして取り出せる。
このように:
> result = (pure 10 :: Writer [String] Int) >>= (\x -> writer (x * 2, ["twice"])) >>= (\x -> writer (x * x, ["square"]))
> runWriter result
(400,["twice","square"])
それぞれの値を取り出したければ fst, snd を使って:
> fst $ runWriter result
400
> snd $ runWriter result
["twice","square"]
以上で、Writer モナドを扱う上で必要な情報は得た。 これらを使って、Writer と bind を使った計算、およびその計算結果から 主たる値とおまけの文字列リストを加工した文字列をログとして取り出すコードにまとめる。
writer.hs:
import Control.Monad.Writer
twice :: Int -> Writer [String] Int
twice x = writer (x * 2, ["twice"])
square :: Int -> Writer [String] Int
square x = writer (x * x, ["sqaure"])
-- ログの取得
getLog :: Writer [String] Int -> String
getLog w = foldl1 (\acc item -> acc ++ " and " ++ item) $ snd (runWriter w)
-- 値の取得
getValue :: Writer [String] Int -> Int
getValue w = fst (runWriter w)
result = (pure 10 :: Writer [String] Int) >>= twice >>= square
GHCi で確認:
> :load writer.hs
> result
WriterT (Identity (400,["twice","sqaure"]))
> getValue result
400
> getLog result
"twice and sqaure"
Maybe と Writer モナドに同じような計算をさせてそのコードを比較しました。
Writer を使うことで、bind で変換したときのログを残すことができる。