2010年3月17日水曜日

Haskell の mapM_ – foldr と (>>) を意識して

1. mapM_ 関数は、どのように定義されているのか?

Haskell の print 関数で文字列を連続して出力させたい場合、次のように書く。

main = do print "hoge"
          print "piyo"
          print "fuga"

これを短かく書きたいなら、

main = mapM_ print ["hoge", "piyo", "fuga"]

最初、意味もわからず mapM_ の動作を覚えようとした。 ^^;

なぜこのように書けるのだろう?

 

2. map, fold 系の復習から

まずは map, fold 系の復習から。

map 関数は「各要素へ指定された関数 f を適用」する。

img02-11-2010[1]

リストの要素を 2 倍するなら、

*Main> map (*2) [0..5]
[0,2,4,6,8,10]

これに対して、fold 系 の関数は大雑把なイメージとして、「各要素のに二項演算子 f を挟んだ後全体の計算をする」。

img02-11-2010[2]

リストの要素を足し合わせるには、

*Main> foldr (+) 0 [0..5]
15

リストの要素を 2 倍して、足し合わせるなら、関数合成を利用して、

*Main> foldr ((+).(*2)) 0 [0..5]
30

( cf. Haskell で関数合成 (2) )

 

3. map と foldr を使って、do 式を置き換える

最初の print 関数を 3 つ並べた例に戻る。

do 式 を使っていたので、これを Monad の (>>) で置き換えると、

main = print "hoge" >> print "piyo" >> print "fuga"

ところで、以下のリストに map 関数を適用した場合、

… map print ["hoge", "piyo", "fuga"]

次のリストが生成される。

[print “hoge”, print “piyo”, print “fuga”]

このリストの要素の間に二項演算子 (>>) を挿入すれば先ほどの形になる。

img03-17-2010[1]

よって、foldr を使えば、

main = foldr1 (>>) $ map print ["hoge", "piyo", "fuga"]

と書ける。

 

4. sequence_ と mapM_ 関数

以下のリストの型は [IO ()] 。

[print “hoge”, print “piyo”, print “fuga”]

IO はモナドの一種。

()

は、モナドな計算をつなげたときに、「前の計算の結果が、続く計算に必要ない」ことを示すために使われる印である「ユニット」。

 

sequence
  1. モナドな計算のリストを評価し、
  2. 結果を一つのリストにまとめ、
  3. モナドで包む関数

sequence

sequence :: Monad m => [m a] -> m [a]

( cf. Haskell におけるモナドのサポート の直列化関数 )

img03-17-2010[2]

返される値に関心がない場合は、末尾にアンダーバー付きの関数。

sequence_ :: Monad m => [m a] -> m ()

これを用いて、上記 foldr1 関数を使った定義を、次のように書換えることができる。

main = sequence_ $ map print ["hoge", "piyo", "fuga"]
  1. 文字列のリストの各要素を print 関数で IO モナドで包み、
  2. 各モナドな計算を評価し、
  3. 結果を必要としない。

 

mapM

この sequence と map を使って定義してある関数が mapM

mapM :: Monad m => (a -> m b) -> [a] -> m [b]

定義 を見ると、

mapM_ f as      =  sequence_ (map f as)
  1. map 関数に、モナドで包む関数を渡し、
  2. リストの各要素に適用した後、
  3. sequence 関数で、一つにまとめモナドで包んでいる

返される値に関心がない場合は、

mapM_ :: Monad m => (a -> m b) -> [a] -> m ()

よって、最終的には、

main = mapM_ print ["hoge", "piyo", "fuga"]

と書ける。あ~、ややこしい。。 (+_+)

 

5. State モナドの場合も同様に考える

上記は、IO モナドにおける mapM_ の適用例だった。今度は、同じモナドの仲間である、State モナドで考えてみる。

080329-004毎度おなじみの Person 型を以下のように定義。

data Person = Person { name :: String
                     , age  :: Int
                     } deriving Show

次に、Person 型の値のリストを保持する Group 型を定義。

data Group = Group [Person] deriving Show

Group 型の値に Person 型の値を追加する addPerson 関数を定義する。

addPerson0              :: Person -> Group -> Group
addPerson0 p (Group ps) = Group (ps++[p])

この関数を State モナド で包みたい。型が s –> (a, s) となるように調整する。

addPerson              :: Person -> Group -> ((), Group)
addPerson p (Group ps) = ((), Group (ps++[p]))

( cf. Haskell の State モナド (1) - 状態を模倣する )

State モナドを使うためには、State モジュールをインポートすることが必要。

import Control.Monad.State

addPerson 関数を使い、あるグループの値に 「太郎、次郎、花子」 を追加する関数 addPs0 を定義してみる。

addPs0 = do State $ addPerson $ Person "Tarou"  10
            State $ addPerson $ Person "Jirou"  20
            State $ addPerson $ Person "Hanako" 30

人が空っぽのグループ型の値を予め定義しておき、

g = Group []

addPs0 関数を試してみる。State モナドに包まれた関数を取り出すのは、runState なので、

*Main> runState addPs0 g
((),Group [Person {name = "Tarou", age = 10},Person {name = "Jirou", age = 20},Person {name = "Hanako", age = 30}])

(cf. jutememo's gist: 333569 — Gist )

 

mapM_ 関数へ

addPs0 関数の中身が不恰好なので、共通部分を抽出する。

  1. 「名前と年齢」のタプルを与えたら、
  2. Person 型の値を生成し、
  3. その値を addPerson 関数に与え、
  4. State モナドで包む

addPs’ 関数を定義。

addPs' (n,a) = State $ addPerson $ Person n a

関数の型を確認すると、

addPs' :: (String, Int) -> State Group ()

先ほどの print 関数の型と比較すると、

print :: (Show a) => a -> IO ()

包むモナドの種類が違うけれど、どちらも mapM_ の第1引数に与えることができる型。

mapM_ :: (Monad m) => (a -> m b) -> [a] -> m ()

addPs' を用いると、先ほどの定義が少しすっきりする。

addPs0 = do addPs' ("Tarou",10)
            addPs' ("Jirou",20)
            addPs' ("Hanko",30)

do 式を使わないなら、

addPs0 = addPs' ("Tarou",10) >> addPs' ("Jirou",20) >> addPs' ("Hanko",30)

先ほどと同じく foldr で置き換えるなら、

addPs0 = foldr1 (>>) $ map addPs' [("Tarou",10), ("Jirou",20), ("Hanko",30)]

sequence 関数を使うと、

addPs0 = sequence $ map addPs' [("Tarou",10), ("Jirou",20), ("Hanko",30)]

先ほどの print 関数を、mapM_ に渡したのを思い出し、addPs1 関数を定義。

addPs1 = mapM_ addPs' [("Tarou",10), ("Jirou",20), ("Hanko",30)]

これですっきりした。 ^^  しかし、いきなりこれ見たら、さっぱりわがんね。。 (+_+)

試しに使ってみる。State モナドの計算の結果のうち、状態の変化のみに関心があるので、今度は runState ではなく execState を使う。

*Main> execState addPs1 g
Group [Person {name = "Tarou", age = 10},Person {name = "Jirou", age = 20},Person {name = "Hanko", age = 30}]

( cf. jutememo's gist: 333569 — Gist )

 

ファイルからデータを読み込む

上記では、Person 型の値をハードコードしていた。これを、ファイルからデータを読み込むように変更してみる。

ファイルには次のようにデータが書かれていたとする。

"Tarou",10
"Jirou",20
"Hanko",30

文字列から代数的データ型に変換するには read 関数を使えばよい。

方法は、文字列から一度タプルにして読み込み、Person 型へと変換する。

文字列 → タプル → Person 型

文字列からタプルへと変換する関数は、

toTuple    :: String -> (String, Int)
toTuple cs = read line where line = '(' : cs ++ ")"

これを使うと、

main = do cs <- getContents
          print $ (execState $ mapM_ addPs' $ map toTuple $ lines cs) g

( cf. jutememo's gist: 333569 — Gist )

しかし、今はまだこれ見直しても何が書いてるかすぐにわからないなぁ。。 (@_@;

 

参考サイト