M.Hiroi's Home Page

Functional Programming

お気楽 Haskell プログラミング入門

[ PrevPage | Haskell | NextPage ]

モナド (2)

前回に引き続きモナドのお話です。山本和彦さんモナモナ言わないモナド入門第二版 によると、モナドは「失敗系」と「状態系」の 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 に追加して返します。

それでは実行してみましょう。

*Main> fibo 0
(0,["fibo 0 called","fibo 0 = 1"])
*Main> fibo 1
(1,["fibo 1 called","fibo 1 = 1"])
*Main> fibo 2
(1,["fibo 2 called","fibo 1 called","fibo 1 = 1","fibo 0 called","fibo 0 = 1","fibo 2 = 1"])
*Main> 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 モナドの使い方

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 を使って値を取り出します。ログを書き込む操作をモナドが行ってくれるのでプログラムはわかりやすくなります。

それでは実行してみましょう。

*Main> runWriter $ fibo' 0
(0,["fibo 0 called","fibo 0 = 0"])
*Main> runWriter $ fibo' 1
(1,["fibo 1 called","fibo 1 = 1"])
*Main> runWriter $ fibo' 2
(1,["fibo 2 called","fibo 1 called","fibo 1 = 1","fibo 0 called","fibo 0 = 0","fibo 2 = 1"])
*Main> 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"

●自分で Writer モナドを作る

それでは 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 で返します。

それでは実行してみましょう。

*Main> runWriter $ fibo'' 0
(0,Sum {getSum = 1})
*Main> runWriter $ fibo'' 1
(1,Sum {getSum = 1})
*Main> runWriter $ fibo'' 2
(1,Sum {getSum = 3})
*Main> runWriter $ fibo'' 3
(2,Sum {getSum = 5})
*Main> runWriter $ fibo'' 4
(3,Sum {getSum = 9})
*Main> runWriter $ fibo'' 10
(55,Sum {getSum = 177})
*Main> 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 は定義どおりにプログラムしただけです。それでは、具体的な例を示しましょう。

*Main> :t ((*2) `bind` \x -> (+10) `bind` \y -> ret (x + y))
((*2) `bind` \x -> (+10) `bind` \y -> ret (x + y))
  :: Num b => b -> b
*Main> (((*2) `bind` \x -> (+10) `bind` \y -> ret (x + y))) 10
40
*Main> (((*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 になります。

関数のモナドの簡単な実行例を示します。

Prelude> ((*2) >>= \x -> (+10) >>= \y -> return (x+y)) 10
40
Prelude> ((*2) >>= \x -> (+10) >>= \y -> return (x+y)) 1
13
Prelude> (do {x <- (*2); y <- (+10); return (x + y)}) 10
40
Prelude> (do {x <- (*2); y <- (+10); return (x + y)}) 1
13

関数 (*2) と (+10) に共通の引数が渡されていることがよくわかると思います。

●Reader モナド

関数のモナドの場合、引数の値は大域変数のように各関数から参照されています。このような動作を「Reader モナド」といいます。Haskell のモジュール Contorl.Monad.Reader には Reader モナドを表すデータ型 Reader e a が定義されています。型変数 e が引数の型、a が返り値の型を表します。Reader モナドの引数は「環境 (environment)」と呼ばれることがあります。

環境は関数 ask を使ってアクセスします。簡単な例を示しましょう。

Prelude> :m + Control.Monad.Reader
Prelude Control.Monad.Reader> runReader ask 0
0
Prelude Control.Monad.Reader> 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 を実行すると次のようになります。

*Main> runReader test 100
(110,210,100)

環境の値は 100 なので、最初の add10 は 110 になり、次の local (+100) add10 は 210 になります。最後の値は 100 なので、local で変更された環境の値は一時的なものであることがわかります。

●自分で Reader モナドを作る

それでは 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 を適用します。

簡単な実行例を示します。

*Main> runReader ask 0
0
*Main> runReader (ask `bind` \x -> ret (x + 10)) 0
10
*Main> 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 してください。

簡単な使用例を示します。

Prelude> :m + Data.Maybe
Prelude Data.Maybe> :t fromMaybe
fromMaybe :: a -> Maybe a -> a
Prelude Data.Maybe> fromMaybe 0 (Just 10)
10
Prelude Data.Maybe> fromMaybe 0 Nothing
0
Prelude Data.Maybe> fromMaybe "" (Just "hello, world")
"hello, world"
Prelude Data.Maybe> fromMaybe "" Nothing
""

それでは、lookupPrice を実行してみましょう。

*Main> runReader (lookupPrice Apple) priceList
100
*Main> runReader (lookupPrice Grape) priceList
150
*Main> runReader (lookupPrice Orange) priceList
200

●State モナド

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 に格納して、スタックの状態を引き継いでいくことに注意してください。

それでは実行してみましょう。

*Main> test' [1,2,3]
((),[3,3])
*Main> test' [4,5]
((),[9])
*Main> 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 記法を使うと、状態を変更するプログラムを簡単に作ることができます。

それでは実行してみましょう。

*Main> runState stackTest [1,2,3]
((),[3,3])
*Main> runState stackTest [4,5]
((),[9])
*Main> runState stackTest $ snd $ runState stackTest [1,2,3]
((),[6])

●get と put

State モナドには、状態を参照・更新する汎用の関数 get, put が用意されています。簡単な使用例を示します。

Prelude Control.Monad.State> runState get [1,2,3]
([1,2,3],[1,2,3])
Prelude Control.Monad.State> runState (put []) [1,2,3]
((),[])
Prelude Control.Monad.State> 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) に変更するだけです。

実行結果を示します。

*Main> runState stackTest' [1,2,3]
((),[3,3])
*Main> runState stackTest' [4,5]
((),[9])
*Main> runState stackTest' $ snd $ runState stackTest' [1,2,3]
((),[6])

●自分で State モナドを作る

それでは 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 を新しい状態としてタプルに格納して返します。

それでは実行してみましょう。

*Main> runState get [1,2,3]
([1,2,3],[1,2,3])
*Main> runState (put []) [1,2,3]
((),[])
*Main> 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)

実行例を示します。

*Main> runState stackTest [1,2,3]
((),[3,3])
*Main> runState stackTest $ snd $ runState stackTest [1,2,3]
((),[6])

●便利なモナディック関数

参考文献 1 によると、モナドを操作する便利な関数のことを「モナディック関数」というそうです。簡単な入出力 で説明した関数 sequence, mapM, mapM_ は I/O アクションだけではなくモナドであれば適用することができます。簡単な実行例を示します。

sequence :: Monad m => [m a] -> m [a]
Prelude> sequence [Just 1, Just 2, Just 3, Just 4]
Just [1,2,3,4]
Prelude> 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]]
Prelude> sequence [print 1, print 2, print 3]
1
2
3
[(),(),()]
Prelude> 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 ()
Prelude> mapM (\x -> Just (x * 2)) [1..5]
Just [2,4,6,8,10]
Prelude> mapM (\x -> [x * 2]) [1..5]
[[2,4,6,8,10]]
Prelude> mapM print [1..5]
1
2
3
4
5
[(),(),(),(),()]
Prelude> 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 を適用するところが異なります。

●liftM

モジュール 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 に変えただけです。簡単な使用例を示します。

Prelude Control.Monad> liftM (*2) (Just 1)
Just 2
Prelude Control.Monad> liftM (*2) Nothing
Nothing
Prelude Control.Monad> liftM (*2) [1..5]
[2,4,6,8,10]
Prelude Control.Monad> liftM (*2) []
[]
Prelude Control.Monad> 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

関数 ap は Applicative の演算子 <*> と同じ働きをモナドに対して行います。

(<*>) :: Applicative f => f (a -> b) -> f a -> f b
ap :: Monad m => m (a -> b) -> m a -> m b

簡単な使用例を示します。

Prelude Control.Monad> return (*2) `ap` Just 1
Just 2
Prelude Control.Monad> return (*) `ap` Just 1 `ap` Just 10
Just 10
Prelude Control.Monad> return (*) `ap` Just 1 `ap` Nothing
Nothing
Prelude Control.Monad> return (*2) `ap` [1..5]
[2,4,6,8,10]
Prelude Control.Monad> return (*) `ap` [1,2,3] `ap` [4,5,6]
[4,5,6,8,10,12,12,15,18]
Prelude Control.Monad> return (*) `ap` [1,2,3] `ap` []
[]
Prelude Control.Monad> [(*2), (+10)] `ap` [1,2,3]
[2,4,6,11,12,13]
Prelude Control.Monad> [(*), (+)] `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]
Prelude Control.Monad> 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

簡単な使用例を示します。

Prelude Control.Monad> liftM2 (+) (Just 1) (Just 2)
Just 3
Prelude Control.Monad> liftM2 (+) [1,2,3] [4,5,6]
[5,6,7,6,7,8,7,8,9]
Prelude Control.Monad> 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 は入れ子になったモナドを平坦化する関数です。

join :: Monad m => m (m a) -> m a

簡単な使用例を示します。

Prelude Control.Monad> join (Just (Just 2))
Just 2
Prelude Control.Monad> join Nothing
Nothing
Prelude Control.Monad> join [[1,2,3],[4,5]]
[1,2,3,4,5]
Prelude Control.Monad> join []
[]

あるデータ型 m がモナドでかつ Functor であるとき、次の関係が成り立ちます。

m >>= f = join (fmap f m)

関数 f はモナドを返し、fmap は文脈を保ちながら関数を適用するので、fmap f m の返り値はモナドの入れ子になるのです。簡単な例を示しましょう。

Prelude Control.Monad> fmap (\x -> Just (x*2)) (Just 10)
Just (Just 20)
Prelude Control.Monad> join $ fmap (\x -> Just (x*2)) (Just 10)
Just 20
Prelude Control.Monad> 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

関数 filterM は filter のモナドバージョンです。

filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]

filterM はリストにフィルターをかけるところは filter と同じですが、その結果のリストをモナドに格納して返すところが異なります。また、述語は真偽値を直接返すのではなく、真偽値をモナドに格納して返します。

簡単な例を示します。

Prelude Control.Monad> filterM (\x -> Just (x < 5)) [1..10]
Just [1,2,3,4]
Prelude Control.Monad> filterM (\x -> Just (even x)) [1..10]
Just [2,4,6,8,10]
Prelude Control.Monad> filterM (\x -> [even x]) [1..10]
[[2,4,6,8,10]]
Prelude Control.Monad> 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

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 になります。簡単な使用例を示します。

Prelude Control.Monad> foldM (\a x -> Just (a + x)) 0 [1..10]
Just 55
Prelude Control.Monad> foldM (\a x -> if x > 5 then Nothing else Just (a + x)) 0 [1..10]
Nothing
Prelude Control.Monad> foldM (\a x -> [a + x]) 0 [1..10]
[55]
Prelude Control.Monad> 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 を満たす必要があります。実際に試してみましょう。

Prelude> fmap (*2) (Just 10)
Just 20
Prelude> Just 10 >>= return . (*2)
Just 20
Prelude> fmap (*2) [1..5]
[2,4,6,8,10]
Prelude> [1..5] >>= return . (*2)
[2,4,6,8,10]
Prelude> fmap reverse getLine
hello, world
"dlrow ,olleh"
Prelude> getLine >>= return . reverse
hello, world
"dlrow ,olleh"

正しく動作していますね。

Haskell のリファレンスマニュアル (Prelude や Control.Monad など) によると、モナドが満たすべき規則は次の 3 通りがあるそうです。

  1. return a >>= k == k a
  2. m >>= return == m
  3. m >>= (\x -> k x >>= h) == (m >>= k) >>= h

1 の規則を左恒等性、2 の規則を右恒等性、3 の規則を結合法則というそうです。規則の詳しい説明は本稿の範囲を超えるので (M.Hiroi の力不足なので)、ここでは Maybe を使って、規則が成り立っていることを示すだけにとどめます。これらの規則の意味は、参考文献 1 や shelarcy さんの 本物のプログラマは Haskell を使う 第3回 mapからモナドを理解する で詳しく説明されています。そちらをお読みくださいませ。

Prelude> return 10 >>= Just
Just 10
Prelude> Just 10 >>= return
Just 10
Prelude> Just 10 >>= (\x -> Just (x * 2) >>= (\y -> Just (y + 10)))
Just 30
Prelude> (Just 10 >>= \x -> Just (x * 2)) >>= \y -> Just (y + 10)
Just 30

初版 2013 年 3 月 31 日
改訂 2021 年 1 月 24 日

Copyright (C) 2013-2021 Makoto Hiroi
All rights reserved.

[ PrevPage | Haskell | NextPage ]