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 を適用」する。
リストの要素を 2 倍するなら、
*Main> map (*2) [0..5] [0,2,4,6,8,10]
これに対して、fold 系 の関数は大雑把なイメージとして、「各要素の間に二項演算子 f を挟んだ後全体の計算をする」。
リストの要素を足し合わせるには、
*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”]
このリストの要素の間に二項演算子 (>>) を挿入すれば先ほどの形になる。
よって、foldr を使えば、
main = foldr1 (>>) $ map print ["hoge", "piyo", "fuga"]
と書ける。
4. sequence_ と mapM_ 関数
以下のリストの型は [IO ()] 。
[print “hoge”, print “piyo”, print “fuga”]
IO はモナドの一種。
()
は、モナドな計算をつなげたときに、「前の計算の結果が、続く計算に必要ない」ことを示すために使われる印である「ユニット」。
sequence
- モナドな計算のリストを評価し、
- 結果を一つのリストにまとめ、
- モナドで包む関数
は sequence 。
sequence :: Monad m => [m a] -> m [a]
( cf. Haskell におけるモナドのサポート の直列化関数 )
返される値に関心がない場合は、末尾にアンダーバー付きの関数。
これを用いて、上記 foldr1 関数を使った定義を、次のように書換えることができる。
main = sequence_ $ map print ["hoge", "piyo", "fuga"]
- 文字列のリストの各要素を print 関数で IO モナドで包み、
- 各モナドな計算を評価し、
- 結果を必要としない。
mapM
この sequence と map を使って定義してある関数が mapM 。
mapM :: Monad m => (a -> m b) -> [a] -> m [b]
定義 を見ると、
mapM_ f as = sequence_ (map f as)
- map 関数に、モナドで包む関数を渡し、
- リストの各要素に適用した後、
- sequence 関数で、一つにまとめモナドで包んでいる
返される値に関心がない場合は、
よって、最終的には、
main = mapM_ print ["hoge", "piyo", "fuga"]
と書ける。あ~、ややこしい。。 (+_+)
5. State モナドの場合も同様に考える
上記は、IO モナドにおける mapM_ の適用例だった。今度は、同じモナドの仲間である、State モナドで考えてみる。
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 関数の中身が不恰好なので、共通部分を抽出する。
- 「名前と年齢」のタプルを与えたら、
- Person 型の値を生成し、
- その値を addPerson 関数に与え、
- 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 )
しかし、今はまだこれ見直しても何が書いてるかすぐにわからないなぁ。。 (@_@;
0コメント:
コメントを投稿