もなもなモナド ReaderモナドとWriterモナドの合成

モナドの合成はさっぱりわからないままだったので、試しにReaderモナドとWriterモナドの合成をやってみた。

リーフは数値、ブランチにはラベルがついてる木構造データを対象に、

  1. 木構造データのリーフの合計を求める関数
  2. さらに、Writerモナドを使って足したリーフの数字を順番にログにとる関数
  3. さらに、Readerモナドを使って親のラベルもログに取る関数

の順番で変更が少ないように実装してみたつもり。それぞれmain1,main2,main3に対応する。これでちゃんと動く理屈はあまりちゃんとわかってない。わかったら後で書く。
以下ソース

data T = B String [T] | L Int
sample = B "root" [L 5,B "child" [L 1,L 10],L 2]

--

main1 = sum_leaf sample
sum_leaf (L n) = n
sum_leaf (B _ children) = sum (map sum_leaf children)

--

main2 = runWriter $ sum_leaf_w sample

sum_leaf_w :: T -> Writer [Int] Int
sum_leaf_w (L n) = do
  tell [n]
  return n

sum_leaf_w (B _ children) = do
  n_list <- mapM sum_leaf_w children
  return (sum n_list)

--

main3 = runReader (runWriterT (sum_leaf_wr sample)) ""

sum_leaf_wr :: T -> WriterT [String] (Reader String) Int
sum_leaf_wr (L n) = do
  parent <- lift ask
  tell ["parent:" ++ parent ++ ", " ++ show n]
  return n

sum_leaf_wr (B parent children) = do
  n_list <- mapM (liftM (local (const parent)) sum_leaf_wr) children
  return (sum n_list)

なんとなく、liftとliftMが対応している、ような気がする。

実行するとこうなる。

Main> main1
18
Main> main2
(18,[5,1,10,2])
Main> main3
(18,["parent:root, 5","parent:child, 1","parent:child, 10","parent:root, 2"])

最初、sum_leaf_wrの中を

mapM (\c->liftM (local (const parent)) (sum_leaf_wr c)) children

と書いてて型があわなくてはまった。むずかしい。

あれーでも、なんでこれで型があってるんだろう。

Main> :t sum_leaf_wr
sum_leaf_wr :: T -> WriterT [String] (Reader String) Int
Main> :t liftM
liftM :: (Monad m) => (a1 -> r) -> m a1 -> m r

T->m ..とm a1って整合するの??

追記 いろいろおしえてもらえた

ありがとうございます。

西川さんからのトラックバックによると、上の例だとliftとliftMは必要ないらしい。

main4 = runReader (runWriterT (sum_leaf_wr' sample)) ""

sum_leaf_wr' :: T -> WriterT [String] (Reader String) Int
sum_leaf_wr' (L n) = do
  parent <- ask
  tell ["parent:" ++ parent ++ ", " ++ show n]
  return n

sum_leaf_wr' (B parent children) = do
  n_list <- mapM (\c-> local (const parent) (sum_leaf_wr' c)) children
  return (sum n_list)
 *Main> main4
(18,["parent:root, 5","parent:child, 1","parent:child, 10","parent:root, 2"])

おお。ほんとだ。MonadReaderのインスタンスと合成*1したWriterTはもとからMonadReaderのインスタンス、ってことか、な?
というより、二つのモナド入れ子的に考える必要はなくて、合成したモナドそのものが両方のモナドの役割を果せる*2ってのが嬉しさなわけか。そこを把握できてなかったよ。

とすると、ReaderとWriterの型の変換順を変えてもコードはほとんど変更する必要ないんだな。

main4' = runWriter (runReaderT (sum_leaf_rw sample) "") -- ここの順番変更

sum_leaf_rw :: T -> ReaderT String (Writer [String]) Int -- 型の順番変更
sum_leaf_rw (L n) = do
  parent <- ask
  tell ["parent:" ++ parent ++ ", " ++ show n]
  return n

sum_leaf_rw (B parent children) = do
  n_list <- mapM (\c-> local (const parent) (sum_leaf_rw c)) children
  return (sum n_list)
 *Main> main4'
(18,["parent:root, 5","parent:child, 1","parent:child, 10","parent:root, 2"])

おお。組み合わせるモナドによっては意味が変わっちゃう場合があるらしいので注意が必要だけど、ReaderとWriterなら気にしなくてよい、と。

shelarcyさんにも言及してもらえました(http://page.freett.com/shelarcy/log/2007/diary_11.html#simotuki302007)。
「本物のプログラマHaskellを使う(http://itpro.nikkeibp.co.jp/article/COLUMN/20060915/248215/?ST=develop)」の次回の予定がモナド変換子の話だそうで。そこまで寝て待とう。

*1:合成、合成、って書いてるけど「変換」のほうが正しい?

*2:それぞれのモナドの性質をもった型クラス両方のインスタンスになれる