前回に引き続きモナドのお話です。山本和彦さんの「モナモナ言わないモナド入門第二版」によると、モナドは「失敗系」と「状態系」の 2 つに分けることができるそうです。失敗系の代表は前回説明した Maybe モナドとリストモナドがあります。状態系の代表には IO モナドがあります。今回は状態系の中から基本的なモナドである Writer モナド、Reader モナド、State モナドについて説明します。
Writer モナドは、ある値 a とそれに付随する値 w から構成されていて、何らかの情報を w に書き込む操作を行います。一般的には、w にログを書き込んでいくような使い方をします。
たとえば、フィボナッチ関数において関数呼び出しの履歴を取ることを考えてみましょう。Writer モナドを使わない場合は、累積変数を使って履歴を取ることになります。次のリストを見てください。
リスト : フィボナッチ関数の履歴 fibo :: Int -> (Int, [String]) fibo n = fibo' n [] where fibo' n xs = let ys = xs ++ ["fibo " ++ show n ++ " called"] in if n < 2 then (n, ys ++ ["fibo " ++ show n ++ " = 1"]) else let (a, zs1) = fibo' (n - 1) ys (b, zs2) = fibo' (n - 2) zs1 c = a + b in (c, zs2 ++ ["fibo " ++ show n ++ " = " ++ show c])
fibo の返り値の型は (Int, [String]) で、第 1 要素が関数の値、第 2 要素が履歴を表す文字列を格納したリストです。実際の処理は局所関数 fibo' で行います。第 2 引数のリスト xs に履歴を表す文字列を格納します。
最初に fibo' が呼び出されたことを表す文字列 "fibo " ++ show n ++ " called" を xs に追加して ys にセットします。次に、n が 2 未満の場合、返り値は 1 になるので、ys に "fibo " ++ show n ++ " = 1" を追加して返します。そうでなければ、fibo' (n - 1) ys を再帰呼び出しして、その結果を (a, zs1) に受け取ります。次に fibo' (n - 2) zs1 を再帰呼び出しして、その結果を (b, zs2) に受け取ります。返り値は a + b になるので、その結果を表す文字列を zs2 に追加して返します。
それでは実行してみましょう。
ghci> fibo 0 (0,["fibo 0 called","fibo 0 = 1"]) ghci> fibo 1 (1,["fibo 1 called","fibo 1 = 1"]) ghci> fibo 2 (1,["fibo 2 called","fibo 1 called","fibo 1 = 1","fibo 0 called","fibo 0 = 1","fibo 2 = 1"]) ghci> mapM_ print $ snd $ fibo 4 "fibo 4 called" "fibo 3 called" "fibo 2 called" "fibo 1 called" "fibo 1 = 1" "fibo 0 called" "fibo 0 = 1" "fibo 2 = 1" "fibo 1 called" "fibo 1 = 1" "fibo 3 = 2" "fibo 2 called" "fibo 1 called" "fibo 1 = 1" "fibo 0 called" "fibo 0 = 1" "fibo 2 = 1" "fibo 4 = 3"
このように、ログを格納するリストを引数に渡していくことで関数呼び出しの履歴を取ることができます。
Writer モナドを使うとプログラムは次のようになります。
リスト : フィボナッチ関数の履歴 (2) import Control.Monad.Writer fibo' :: Int -> Writer [String] Int fibo' n = do tell ["fibo " ++ show n ++ " called"] if n < 2 then do tell ["fibo " ++ show n ++ " = " ++ show n] return n else do a <- fibo' (n - 1) b <- fibo' (n - 2) tell ["fibo " ++ show n ++ " = " ++ show (a + b)] return (a + b)
Writer モナドはモジュール Control.Monad.Writer に定義されています。データ型は Writer w a で、w がログを表す型変数です。Writer 型の実体はタプル (a, w) です。なお、w はモノイドでなければいけません。<- で Writer w a の値 a を取り出し、関数 tell でデータをログ w に追加します。Writer はデータ構築子を非公開にしているので、関数 runWriter を使って値を取り出します。ログを書き込む操作をモナドが行ってくれるのでプログラムはわかりやすくなります。
それでは実行してみましょう。
ghci> runWriter $ fibo' 0 (0,["fibo 0 called","fibo 0 = 0"]) ghci> runWriter $ fibo' 1 (1,["fibo 1 called","fibo 1 = 1"]) ghci> runWriter $ fibo' 2 (1,["fibo 2 called","fibo 1 called","fibo 1 = 1","fibo 0 called","fibo 0 = 0", "fibo 2 = 1"]) ghci> mapM_ print $ snd $ runWriter $ fibo' 4 "fibo 4 called" "fibo 3 called" "fibo 2 called" "fibo 1 called" "fibo 1 = 1" "fibo 0 called" "fibo 0 = 0" "fibo 2 = 1" "fibo 1 called" "fibo 1 = 1" "fibo 3 = 2" "fibo 2 called" "fibo 1 called" "fibo 1 = 1" "fibo 0 called" "fibo 0 = 0" "fibo 2 = 1" "fibo 4 = 3"
それでは Haskell の勉強ということで、自分で Writer モナドを作ってみましょう。実際の Writer モナドはちょっと複雑な方法でプログラムされていますが、newtype で Writer 型を定義して、前回作成した Mmonad のインスタンスにする方法であれば、簡単にプログラムすることができます。
リスト : Writer モナド import Data.Monoid -- データ型の定義 newtype Writer w a = Writer {runWriter :: (a, w)} -- インスタンスの設定 instance Monoid w => Mmonad (Writer w) where ret x = Writer (x, mempty) (Writer (x, v)) `bind` f = let Writer (y, v') = f x in Writer (y, v `mappend` v') -- ログの書き込み tell :: Monoid w => w -> Writer w () tell s = Writer ((), s)
データ型の定義は簡単ですね。インスタンスの設定では、型クラス制約でログを格納する型変数 w はモノイドであることを指定します。ret x も簡単で、Writer (x, mempty) を返すだけです。bind の定義は、Writer (x, v) から値 x を取り出して、関数 f に渡して評価します。
その結果は Writer 型になるので、パターンマッチングで返り値 y とログ v' を取り出します。そして、v と v' を mappend で連結した結果を Writer に格納して返します。ログを追加する関数 tell も簡単です。unit と引数 s をタプルにセットして、それを Writer に格納して返すだけです。
簡単な例として、フィボナッチ関数の呼び出し回数を求めるプログラムを作ってみましょう。次のリストを見てください。
リスト : フィボナッチ関数の呼び出し回数を求める fibo'' :: Int -> Writer (Sum Int) Int fibo'' n = tell (Sum 1) `bind` \_ -> if n < 2 then ret 1 else fibo'' (n - 1) `bind` \a -> fibo'' (n - 2) `bind` \b -> ret (a + b)
Mmonad の場合、do 記法を使うことができないので、ちょっと読みにくいプログラムになっています。関数 fibo の型は Int -> Writer (Sum Int) Int になります。Sum Int は加法のモノイドなので、tell (Sum 1) を実行すると、ログに 1 が加算されます。
あとは bind でラムダ式をつないでいきます。n が 2 未満であれば ret 1 を返します。加法の mempty は 0 なので、呼び出し回数が加算されることはありません。そうでなければ、fibo (n - 1) と fibo (n - 2) を bind でつないで呼び出し、その返り値 a, b を加算した値を ret で返します。
それでは実行してみましょう。
ghci> runWriter $ fibo'' 0 (0,Sum {getSum = 1}) ghci> runWriter $ fibo'' 1 (1,Sum {getSum = 1}) ghci> runWriter $ fibo'' 2 (1,Sum {getSum = 3}) ghci> runWriter $ fibo'' 3 (2,Sum {getSum = 5}) ghci> runWriter $ fibo'' 4 (3,Sum {getSum = 9}) ghci> runWriter $ fibo'' 10 (55,Sum {getSum = 177}) ghci> runWriter $ fibo'' 20 (6765,Sum {getSum = 21891})
フィボナッチ関数は二重再帰なので、関数を呼び出す回数は急激に大きくなります。
Functor, Applicative で説明したように -> は中置演算子で、関数の型は (->) r a と書くこともできます。(->) r を型コンストラクタと考えると、関数のモナドの定義を導くことができます。
>>= :: m a -> (a -> m b) -> m b => ((->) r a) -> (a -> (->) r b) -> ((->) r b) => (r -> a) -> (a -> r -> b) -> (r -> b)
f >>= g は関数 f :: r -> a と g :: a -> r -> b を受け取って、関数 r -> b を返します。f は r から a を求め、関数 g は a と r から b を求めることができるので、>>= が返す関数をラムダ式で書くと \r -> g (f r) r と表すことができます。
ここで、関数 g にも引数 r が渡されることに注意してください。このあと、さらに >>= で関数をつないでいくことができます。f >>= g で生成される関数の型は r -> b なので、最初に与えた引数 r が次の関数にも伝播していきます。つまり、引数 r を各関数の共通の変数として利用することができるのです。
それでは前回作成した Mmonad を使って関数のモナドを作ってみましょう。プログラムは次のようになります。
リスト : インスタンスの設定 instance Mmonad ((->) r) where ret x = \_ -> x f `bind` g = \x -> g (f x) x
ret は Applicative の pure と同じで、bind は定義どおりにプログラムしただけです。それでは、具体的な例を示しましょう。
ghci> :t ((*2) `bind` \x -> (+10) `bind` \y -> ret (x + y)) ((*2) `bind` \x -> (+10) `bind` \y -> ret (x + y)) :: Num b => b -> b ghci> (((*2) `bind` \x -> (+10) `bind` \y -> ret (x + y))) 10 40 ghci> (((*2) `bind` \x -> (+10) `bind` \y -> ret (x + y))) 1 13
bind で合成した関数の型は Num b => b -> b になります。ここで、引数 10 を与えると、10 は関数 (*2) と関数 (+10) に渡されて評価されます。10 * 2 の結果が引数 x に、10 + 10 の結果が引数 y に渡されるので、最後の ret (x + y) の値は 40 になります。引数が 1 の場合、値は 13 になります。
関数のモナドの簡単な実行例を示します。
ghci> ((*2) >>= \x -> (+10) >>= \y -> return (x+y)) 10 40 ghci> ((*2) >>= \x -> (+10) >>= \y -> return (x+y)) 1 13 ghci> (do {x <- (*2); y <- (+10); return (x + y)}) 10 40 ghci> (do {x <- (*2); y <- (+10); return (x + y)}) 1 13
関数 (*2) と (+10) に共通の引数が渡されていることがよくわかると思います。
関数のモナドの場合、引数の値は大域変数のように各関数から参照されています。このような動作を「Reader モナド」といいます。Haskell のモジュール Contorl.Monad.Reader には Reader モナドを表すデータ型 Reader e a が定義されています。型変数 e が引数の型、a が返り値の型を表します。Reader モナドの引数は「環境 (environment)」と呼ばれることがあります。
環境は関数 ask を使ってアクセスします。簡単な例を示しましょう。
ghci> :m + Control.Monad.Reader ghci> runReader ask 0 0 ghci> runReader (do {x <- ask; return (x * 2)}) 10 20
Reader 型のデータ構築子は非公開なので、関数 runReader を使って関数を取り出します。最初の例は ask で引数の値 0 を求め、それをそのまま返すことになります。次の例は ask で引数の値を求めて変数 x にセットし、それを 2 倍した値を返すことになります。
関数 local を使うと、環境の値を一時的に変更することができます。次の例を見てください。
リスト : local の使用例 import Control.Monad.Reader add10 :: Reader Int Int add10 = do x <- ask return $ x + 10 test :: Reader Int (Int, Int, Int) test = do x <- add10 y <- local (+100) add10 z <- ask return (x, y, z)
local の第 1 引数は環境を更新する関数、第 2 引数は変更した環境のもとで実行する関数です。関数 add10 は環境の値を +10 した値を返します。関数 test では、最初に add10 を呼び出して、次に local で環境を +100 してから add10 を呼び出し、最後に ask で環境の値を求めます。test を実行すると次のようになります。
ghci> runReader test 100 (110,210,100)
環境の値は 100 なので、最初の add10 は 110 になり、次の local (+100) add10 は 210 になります。最後の値は 100 なので、local で変更された環境の値は一時的なものであることがわかります。
それでは Haskell の勉強ということで、自分で Reader モナドを作ってみましょう。実際の Reader モナドはちょっと複雑な方法でプログラムされていますが、newtype で Reader 型を定義して、前回作成した Mmonad のインスタンスにする方法であれば、簡単にプログラムすることができます。次のリストを見てください。
リスト : Reader モナド -- データ型の定義 newtype Reader e a = Reader {runReader :: e -> a} -- インスタンスの設定 instance Mmonad (Reader e) where ret x = Reader $ \_ -> x (Reader f) `bind` g = Reader $ \e -> runReader (g (f e)) e ask :: Reader a a ask = Reader id local :: (e -> e) -> Reader e a -> Reader e a local f c = Reader $ \e -> runReader c (f e)
データ型の定義は簡単です。Reader e a の中に関数 e -> a を格納するだけです。インスタンスの設定も簡単です。関数のモナドに Reader の皮をかぶせただけです。関数 g の返り値は Reader 型になるので、runReader で関数を取り出して引数 e に適用します。
関数 ask も簡単です。恒等関数 id を Reader に格納するだけです。これで id に渡された引数をそのまま返すことができます。local は引数 e に関数 f を適用して環境を更新し、それに runReader c を適用します。
簡単な実行例を示します。
ghci> runReader ask 0 0 ghci> runReader (ask `bind` \x -> ret (x + 10)) 0 10 ghci> runReader (local (+100) (ask `bind` \x -> ret (x + 10))) 0 110
もう一つ、簡単な例を示しましょう。Reader モナドを使って果物の値段を求めるプログラムを作ります。次のリストを見てください。
リスト : 果物の値段を求める import Data.Maybe data Fruit = Apple | Grape | Orange deriving (Show, Eq) type Price = (Fruit, Int) priceList :: [Price] priceList = [(Apple, 100), (Grape, 150), (Orange, 200)] lookupPrice :: Fruit -> Reader [Price] Int lookupPrice x = ask `bind` \ps -> ret $ fromMaybe 0 (lookup x ps)
果物を表すデータ型 Fruit と果物の値段を表すデータ型 Price を定義します。果物の値段は連想リスト priceList に定義します。lookupPrice は果物 x の値段を求めます。ask で環境の値 ps を求め、関数 lookup で果物 x の値段を ps から探索します。関数 fromMaybe は第 2 引数の Just から値を取り出して返しますが、第 2 引数が Nothing であれば第 1 引数の値を返します。fromMaybe はモジュール Data.Maybe に定義されているので、使用するときは Data.Maybe を import してください。
簡単な使用例を示します。
ghci> :m + Data.Maybe ghci> :t fromMaybe fromMaybe :: a -> Maybe a -> a ghci> fromMaybe 0 (Just 10) 10 ghci> fromMaybe 0 Nothing 0 ghci> fromMaybe "" (Just "hello, world") "hello, world" ghci> fromMaybe "" Nothing ""
それでは、lookupPrice を実行してみましょう。
ghci> runReader (lookupPrice Apple) priceList 100 ghci> runReader (lookupPrice Grape) priceList 150 ghci> runReader (lookupPrice Orange) priceList 200
Writer モナドはログを書き込み、Reader モナドは共通の環境を参照する働きをします。これに対し、State モナドは値のほかに状態 (state) を保持していて、その状態を参照するだけではなく更新していくことができます。
もちろん、Haskell では変数の値を書き換えることはできませんが、モナドを使って新しい状態を引数に渡していくことで、あたかも状態を更新しているかのようにプログラムすることができます。
たとえば、簡単なスタックを考えてみましょう。
リスト : 簡単なスタック type Stack a = [a] pop :: Stack a -> (a, Stack a) pop (x:xs) = (x, xs) pop [] = error "empty stack" push :: Stack a -> a -> ((), Stack a) push xs x = ((), x:xs) test' :: Stack Int -> ((), Stack Int) test' xs = let (a, s1) = pop xs (b, s2) = pop s1 in push s2 (a + b)
スタックはリストで表すことにします。関数 pop はスタックからデータを取り出し、その値と新しいスタックの状態をタプルに格納して返します。関数 push はスタックに新しいデータを追加し、unit と新しいスタックの状態をタプルに格納して返します。
関数 test' はスタックからデータを 2 つ取り出して、それを足し算した結果をスタックに追加して返します。pop するたびにスタックの状態は変化するので、それを変数 s1, s2 に格納して、スタックの状態を引き継いでいくことに注意してください。
それでは実行してみましょう。
ghci> test' [1,2,3] ((),[3,3]) ghci> test' [4,5] ((),[9]) ghci> test' $ snd $ test' [1,2,3] ((),[6])
State モナドを使うと、スタックの状態を変数に保持するプログラムを書く必要がなくなります。
State モナドはモジュール Control.Monad.State に定義されています。データ型は State s a で、s が状態を表す型変数です。State 型の実体は関数 s -> (a, s) です。State はデータ構築子を非公開にしているので、関数 state で State 型の値を生成し、関数 runState を使って値を取り出します。
プログラムは次のようになります。
リスト : State モナドを使ったスタック pop' :: State (Stack Int) Int pop' = state $ \(x:xs) -> (x, xs) push' :: Int -> State (Stack Int) () push' x = state $ \xs -> ((), x:xs) stackTest :: State (Stack Int) () stackTest = do a <- pop' b <- pop' push' (a + b)
push', pop' は state で関数を State に格納します。stackTest は do 記法を使ってプログラムしています。pop' でスタックからデータを取り出して変数 a, b にセットし、a + b の値を push' でスタックに追加して返します。このように、State モナドと do 記法を使うと、状態を変更するプログラムを簡単に作ることができます。
それでは実行してみましょう。
ghci> runState stackTest [1,2,3] ((),[3,3]) ghci> runState stackTest [4,5] ((),[9]) ghci> runState stackTest $ snd $ runState stackTest [1,2,3] ((),[6])
State モナドには、状態を参照・更新する汎用の関数 get, put が用意されています。簡単な使用例を示します。
ghci> runState get [1,2,3] ([1,2,3],[1,2,3]) ghci> runState (put []) [1,2,3] ((),[]) ghci> runState (do {a <- get; put (10:a)}) [1,2,3] ((),[10,1,2,3])
状態を s とすると、関数 get は (s, s) を返し、関数 put s1 は ((), s1) を返します。最後の例で、a <- get は ([1,2,3], [1,2,3]) から先頭要素 [1,2,3] を取り出して変数 a にセットします。put (10:a) は状態を (10:a) に更新した値を返すので、返り値は ((), [10,1,2,3]) となります。
get, put を使うとスタックのプログラムは次のようになります。
リスト : get, put を使う場合 pop'' :: State (Stack Int) Int pop'' = do xs <- get put (tail xs) return (head xs) push'' :: Int -> State (Stack Int) () push'' x = do xs <- get put (x:xs) stackTest' :: State (Stack Int) () stackTest' = do a <- pop'' b <- pop'' push'' (a + b)
pop'' は get を使ってスタックを取り出します。GHC ver 8.8.4 の場合、(x:xs) <- get のようにパターンマッチングで分解するとエラーになるので注意してください。そして、put でスタックの状態を (tail xs) に変更し、return (head xs) で先頭要素を State に格納して返します。push'' はもっと簡単で、get でスタックの状態 xs を取り出して、put で新しい状態 (x:xs) に変更するだけです。
実行結果を示します。
ghci> runState stackTest' [1,2,3] ((),[3,3]) ghci> runState stackTest' [4,5] ((),[9]) ghci> runState stackTest' $ snd $ runState stackTest' [1,2,3] ((),[6])
それでは Haskell の勉強ということで、自分で State モナドを作ってみましょう。実際の State モナドはちょっと複雑な方法でプログラムされていますが、newtype で State 型を定義して、前回作成した Mmonad のインスタンスにする方法であれば、簡単にプログラムすることができます。次のリストを見てください。
リスト : State モナド -- データ型の定義 newtype State s a = State {runState :: s -> (a, s)} -- インスタンスの設定 instance Mmonad (State s) where ret x = State $ \s -> (x, s) (State f) `bind` g = State $ \s -> let (x, s1) = f s in runState (g x) s1 -- 状態を取得する get :: State s s get = State $ \s -> (s, s) -- 状態を更新する put :: s -> State s () put s = State $ \_ -> ((), s)
データ型の定義は簡単ですね。ret と bind の定義は Reader モナドに少し似ています。ただし State モナドの場合、状態 s を次の処理に伝えてく必要があることに注意してください。ret x は x と状態 s をタプル (x, s) にまとめて返します。
bind はちょっと複雑なので、型を展開して考えてみましょう。
bind :: m a -> (a -> m b) -> m b => State s a -> (a -> State s b) -> State s b => State (s -> (a, s)) -> (a -> State (s -> (b, s))) -> State (s -> (b, s))
bind の左辺 State f から関数 f を取り出します。f の型は s -> (a, s) なので、状態 s に関数 f を適用して、値と新しい状態を変数 x, s1 に受け取ります。次に、値 x に関数 g を適用すると State 型が返ってくるので、runState で State に格納されている関数を取り出します。あとは、更新された状態 s1 にその関数を適用すればいいわけです。
get, put は簡単です。get はラムダ式に渡された状態 s をタプルに格納して返すだけです。put はラムダ式に渡された状態を無視して、引数 s を新しい状態としてタプルに格納して返します。
それでは実行してみましょう。
ghci> runState get [1,2,3] ([1,2,3],[1,2,3]) ghci> runState (put []) [1,2,3] ((),[]) ghci> runState (get `bind` \a -> put (10:a)) [1,2,3] ((),[10,1,2,3])
もちろん、スタックの操作も同じようにできます。
リスト : スタックの操作 type Stack a = [a] pop :: State (Stack Int) Int pop = get `bind` \(x:xs) -> put xs `bind` \_ -> ret x push :: Int -> State (Stack Int) () push x = get `bind` \xs -> put (x:xs) stackTest :: State (Stack Int) () stackTest = pop `bind` \a -> pop `bind` \b -> push (a + b)
実行例を示します。
ghci> runState stackTest [1,2,3] ((),[3,3]) ghci> runState stackTest $ snd $ runState stackTest [1,2,3] ((),[6])
参考文献『すごい Haskell たのしく学ぼう!』によると、モナドを操作する便利な関数のことを「モナディック関数」というそうです。「簡単な入出力」で説明した関数 sequence, mapM, mapM_ は I/O アクションだけではなくモナドであれば適用することができます。簡単な実行例を示します。
sequence :: Monad m => [m a] -> m [a]
ghci> sequence [Just 1, Just 2, Just 3, Just 4] Just [1,2,3,4] ghci> sequence [[1,2],[3,4],[5,6]] [[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2,3,5],[2,3,6],[2,4,5],[2,4,6]] ghci> sequence [print 1, print 2, print 3] 1 2 3 [(),(),()] ghci> sequence $ map print [1,2,3] 1 2 3 [(),(),()]
mapM :: Monad m => (a -> m b) -> [a] -> m [b] mapM_ :: Monad m => (a -> m b) -> [a] -> m ()
ghci> mapM (\x -> Just (x * 2)) [1..5] Just [2,4,6,8,10] ghci> mapM (\x -> [x * 2]) [1..5] [[2,4,6,8,10]] ghci> mapM print [1..5] 1 2 3 4 5 [(),(),(),(),()] ghci> mapM_ print [1..5] 1 2 3 4 5
それではここで Haskell の勉強のため、Mmonad を使って sequence と mapM を作ってみましょう。関数名は sequence' と mapM' としました。
リスト : sequence と mapM sequence' :: Mmonad m => [m a] -> m [a] sequence' [] = ret [] sequence' (x:xs) = x `bind` \y -> sequence' xs `bind` \ys -> ret (y:ys) mapM' :: Mmonad m => (a -> m b) -> [a] -> m [b] mapM' _ [] = ret [] mapM' f (x:xs) = f x `bind` \y -> mapM' f xs `bind` \ys -> ret (y:ys)
sequence' は引数が空リストであれば、ret で空リストをモナドに格納して返します。そうでなければ、リストの要素 (モナド) x から bind で値を取り出してラムダ式の引数 y に渡します。次に、sequence' を再帰呼び出しして、その返り値 (モナド) からリストを取り出してラムダ式の引数 ys に渡します。あとは、y を ys に追加して、それを ret でモナドに格納して返します。mapM もほとんど同じですが、f x の返り値 (モナド) に bind を適用するところが異なります。
モジュール Control.Monad にはモナドを操作する便利なモナディック関数が用意されています。関数 liftM は fmap のモナド版です。liftM の型は fmap とよく似ています。
fmap :: Functor f => (a -> b) -> f a -> f b liftM :: Monad m => (a1 -> r) -> m a1 -> m r
liftM の型は fmap の型クラス制約 Functor f を Monad m に変えただけです。簡単な使用例を示します。
ghci> liftM (*2) (Just 1) Just 2 ghci> liftM (*2) Nothing Nothing ghci> liftM (*2) [1..5] [2,4,6,8,10] ghci> liftM (*2) [] [] ghci> liftM reverse getLine hello, world "dlrow ,olleh"
Mmonad を使って liftM をプログラムすると次のようになります。
リスト : liftM liftM :: Mmonad m => (a -> b) -> m a -> m b liftM f x = x `bind` \y -> ret (f y)
引数 x に bind を適用してモナドの値を取り出してラムダ式の引数 y に渡します。あとは、f y の返り値を ret でモナドに格納して返すだけです。
関数 ap は Applicative の演算子 <*> と同じ働きをモナドに対して行います。
(<*>) :: Applicative f => f (a -> b) -> f a -> f b ap :: Monad m => m (a -> b) -> m a -> m b
簡単な使用例を示します。
ghci> return (*2) `ap` Just 1 Just 2 ghci> return (*) `ap` Just 1 `ap` Just 10 Just 10 ghci> return (*) `ap` Just 1 `ap` Nothing Nothing ghci> return (*2) `ap` [1..5] [2,4,6,8,10] ghci> return (*) `ap` [1,2,3] `ap` [4,5,6] [4,5,6,8,10,12,12,15,18] ghci> return (*) `ap` [1,2,3] `ap` [] [] ghci> [(*2), (+10)] `ap` [1,2,3] [2,4,6,11,12,13] ghci> [(*), (+)] `ap` [1,2,3] `ap` [4,5,6] [4,5,6,8,10,12,12,15,18,5,6,7,6,7,8,7,8,9] ghci> return (,) `ap` [1,2,3] `ap` [4,5,6] [(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]
Applicative の関数 liftA2 に対応するモナディック関数 liftM2 もあります。
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
簡単な使用例を示します。
ghci> liftM2 (+) (Just 1) (Just 2) Just 3 ghci> liftM2 (+) [1,2,3] [4,5,6] [5,6,7,6,7,8,7,8,9] ghci> liftM2 (++) getLine getLine hello, world "hello,world"
ap と liftM2 のプログラムは次のようになります。
リスト : ap と liftM2 ap :: Mmonad m => m (a -> b) -> m a -> m b ap f x = f `bind` \f' -> x `bind` \x' -> ret (f' x') liftM2 :: Mmonad m => (a -> b -> c) -> m a -> m b -> m c liftM2 f x y = x `bind` \x' -> y `bind` \y' -> ret (f x' y')
ap は引数 f と x に bind を適用して関数 f' と値 x' を取り出し、f' x' の返り値を ret でモナドに格納して返します。liftM2 も簡単で、引数 x, y に bind を適用して値 x', y' を取り出して、f x' y' の返り値を ret でモナドに格納して返します。このように、Applicative はモナドから作ることもできます。
join は入れ子になったモナドを平坦化する関数です。
join :: Monad m => m (m a) -> m a
簡単な使用例を示します。
ghci> join (Just (Just 2)) Just 2 ghci> join Nothing Nothing ghci> join [[1,2,3],[4,5]] [1,2,3,4,5] ghci> join [] []
あるデータ型 m がモナドでかつ Functor であるとき、次の関係が成り立ちます。
m >>= f = join (fmap f m)
関数 f はモナドを返し、fmap は文脈を保ちながら関数を適用するので、fmap f m の返り値はモナドの入れ子になるのです。簡単な例を示しましょう。
ghci> fmap (\x -> Just (x*2)) (Just 10) Just (Just 20) ghci> join $ fmap (\x -> Just (x*2)) (Just 10) Just 20 ghci> Just 10 >>= \x -> Just (x*2) Just 20
ラムダ式は Just (x*2) を返すので、Just 10 に適用すると結果は Just (Just 20) になります。ここに join を適用すると Just 20 が得られます。これは演算子 >>= を適用した結果と同じになります。このことから、Functor の fmap と関数 join があればモナドの演算子 >>= を定義できることがわかります。
join のプログラムは簡単です。次のリストを見てください。
リスト : join join :: Mmonad m => m (m a) -> m a join x = x `bind` id
引数 x に bind を適用して値を取り出して、それに id を適用すれば、その値 (モナド) をそのまま返すことができます。
関数 filterM は filter のモナドバージョンです。
filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]
filterM はリストにフィルターをかけるところは filter と同じですが、その結果のリストをモナドに格納して返すところが異なります。また、述語は真偽値を直接返すのではなく、真偽値をモナドに格納して返します。
簡単な例を示します。
ghci> filterM (\x -> Just (x < 5)) [1..10] Just [1,2,3,4] ghci> filterM (\x -> Just (even x)) [1..10] Just [2,4,6,8,10] ghci> filterM (\x -> [even x]) [1..10] [[2,4,6,8,10]] ghci> filterM (\x -> [True, False]) [1,2,3] [[1,2,3],[1,2],[1,3],[1],[2,3],[2],[3],[]]
最後の例はリストモナド (非決定性計算) が働くので、結果は「べき集合」になります。
filterM のプログラムは次のようになります。
リスト : filterM filterM :: Mmonad m => (a -> m Bool) -> [a] -> m [a] filterM _ [] = ret [] filterM p (x:xs) = filterM p xs `bind` \ys -> p x `bind` \t -> ret (if t then (x:ys) else ys)
filterM の第 2 引数が空リストであれば、ret で空リストをモナドに格納して返します。そうでなければ、filterM を再帰呼び出しして、その返り値に bind を適用して値 (リスト) ys を取り出します。次に、p x の返り値を bind に適用して真偽値 t を取り出します。t が True であれば x を ys に追加します。あとは ret で結果をモナドに格納して返すだけです。
foldM は畳み込み foldl のモナドバージョンです。
foldl :: (a -> b -> a) -> a -> [b] -> a foldM :: Monad m => (a -> b -> m a) -> a -> [b] -> m a
foldM の場合、関数の返り値がモナド m a で、返り値もモナド m a になります。簡単な使用例を示します。
ghci> foldM (\a x -> Just (a + x)) 0 [1..10] Just 55 ghci> foldM (\a x -> if x > 5 then Nothing else Just (a + x)) 0 [1..10] Nothing ghci> foldM (\a x -> [a + x]) 0 [1..10] [55] ghci> foldM (\a x -> if x > 5 then [] else [a + x]) 0 [1..10] []
foldM のプログラムは次のようになります。
リスト : foldM foldM :: Mmonad m => (a -> b -> m a) -> a -> [b] -> m a foldM _ a [] = ret a foldM f a (x:xs) = f a x `bind` \a' -> foldM f a' xs
第 3 引数が空リストであれば、ret で空リストをモナドに格納して返します。そうでなければ、f a x に bind を適用して累積値 a' を求め、foldM f a' xs を再帰呼び出しするだけです。
このほかにもモジュール Contorl.Monad には便利なモナディック関数が用意されています。詳細は Control.Monad のマニュアルをお読みくださいませ。
モナドにも満たすべき規則があります。まず Functor とモナドに関する規則として、fmap f xs == xs >>= return . f を満たす必要があります。実際に試してみましょう。
ghci> fmap (*2) (Just 10) Just 20 ghci> Just 10 >>= return . (*2) Just 20 ghci> fmap (*2) [1..5] [2,4,6,8,10] ghci> [1..5] >>= return . (*2) [2,4,6,8,10] ghci> fmap reverse getLine hello, world "dlrow ,olleh" ghci> getLine >>= return . reverse hello, world "dlrow ,olleh"
正しく動作していますね。
Haskell のリファレンスマニュアル (Prelude や Control.Monad など) によると、モナドが満たすべき規則は次の 3 通りがあるそうです。
1 の規則を左恒等性、2 の規則を右恒等性、3 の規則を結合法則というそうです。規則の詳しい説明は本稿の範囲を超えるので (M.Hiroi の力不足なので)、ここでは Maybe を使って、規則が成り立っていることを示すだけにとどめます。これらの規則の意味は、参考文献『すごい Haskell たのしく学ぼう!』や shelarcy さんの「本物のプログラマは Haskell を使う 第3回 mapからモナドを理解する」で詳しく説明されています。そちらをお読みくださいませ。
ghci> return 10 >>= Just Just 10 ghci> Just 10 >>= return Just 10 ghci> Just 10 >>= (\x -> Just (x * 2) >>= (\y -> Just (y + 10))) Just 30 ghci> (Just 10 >>= \x -> Just (x * 2)) >>= \y -> Just (y + 10) Just 30