Prev: 練習問題 TOC: 目次 Next: Part II - イントロダクション

Haskell におけるモナドのサポート


Haskell 組み込みでのモナドのサポートは、ほとんどのモナド共通の関数を エクスポートする標準プレリュードとそれよりは共通性のないモナド関数を エクスポートする Monad モジュールに分けられています。個々のモナド型 はそれぞれ独自のライブラリにあり、それがこのチュートリアルの Part IIの主題です。

標準プレリュード

Haskell 98 の 標準プレリュードには、モナドデータ型で働くいくつかの補助関数に 加えて、Monad クラスの定義が含まれています。

Monad クラス

これまでに Monad クラスを見てきました。

class  Monad m  where
    (>>=)  :: m a -> (a -> m b) -> m b
    (>>)   :: m a -> m b -> m b
    return :: a -> m a
    fail   :: String -> m a

        -- Minimal complete definition:
        --      (>>=), return
    m >> k  =  m >>= \_ -> k
    fail s  = error s

直列化関数

sequence 関数はモナド計算のリストを取り、 それぞれを順に実行して、結果のリストを返します。 もしその計算のどれかが失敗すると関数全体が失敗します。

sequence :: Monad m => [m a] -> m [a] 
sequence = foldr mcons (return [])
             where mcons p q = p >>= \x -> q >>= \y -> return (x:y)

sequence_ 関数は(アンダースコアに注意) sequence と同じ振舞いをしますが、結果のリストは 返しません。これはモナド計算の副作用のみが重要な場合に便利です。

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

写像関数

mapM 関数は、モナド計算を値のリスト上に写像し、 結果のリストを返します。これはリスト上の map 関数と上の sequence 関数を使って定義されています。

mapM :: Monad m => (a -> m b) -> [a] -> m [b] 
mapM f as = sequence (map f as)

こちらにも、アンダースコア版があります。mapM_ は、 sequence_ を使って定義されています。mapM_ は、mapM と同じ操作をしますが、値のリストは返しません。 モナド計算の副作用だけが重要な場合に便利です。

mapM_ :: Monad m => (a -> m b) -> [a] -> m () 
mapM_ f as = sequence_ (map f as)

写像関数の単純な例としては、IO モナド用の putString 関数が以下のように定義できます。

putString :: [Char] -> IO ()
putString s = mapM_ putChar s

mapM は、map 関数が通常のリスト上で使えるのと 同様の方法で、do ブロック中で使うことができます。これは、モナド共通の パターンで、— モナド中で使うためのバージョンの関数(すなわち、 束縛を意図している関数)は非モナドバージョンと同様のシグネチャを持つことに なりますが、関数の出力はモナド中にとじこめられます。

-- compare the non-monadic and monadic signatures
map  ::            (a -> b)   -> [a] -> [b]
mapM :: Monad m => (a -> m b) -> [a] -> m [b] 

逆方向束縛関数 (=<<)

プレリュードでは標準の束縛関数とは逆順の引数をとる束縛関数が定義されてい ます。標準の束縛関数は ">>=" ですので、逆束縛関数は "=<<" です。束縛演算子が高階項として使われる場合で、 引数を逆順でもつのが便利な場合があり、その場合に有用です。 定義は単純です。

(=<<) :: Monad m => (a -> m b) -> m a -> m b
f =<< x = x >>= f

Monad モジュール

標準の Haskell 98 ライブラリの Monad モジュールは より高度なモナド演算用いくつもの便利な関数をエクスポートしています。 これらの便利な関数を使うには単に Haskell のプログラム中に import Monad とするだけです。

MonadPlus クラス

Monad モジュールではゼロ要素とプラス演算子をもつ モナドに対して MonadPlus クラスが定義されています。

class Monad m => MonadPlus m where
    mzero :: m a
    mplus :: m a -> m a -> m a

リスト関数のモナド版

標準のリスト処理関数をモナドに一般化する関数がいくつか定義されています。 mapM 関数は、標準プレリュードでエクスポートされています。 その内容は前述のとおりです。

foldMfoldl のモナド版で、リストから構築 されたモナド計算は左から右へ束縛されます。その定義は、

foldM :: (Monad m) => (a -> b -> m a) -> a -> [b] -> m a
foldM f a []     = return a
foldM f a (x:xs) = f a x >>= \y -> foldM f y xs

です。しかし、do ブロックの作用を考えれば、foldM の操作を 理解するのはもっと容易でしょう。

-- これは正しい Haskell のコードではなく、説明のためだけのものです
foldM f a1 [x1,x2,...,xn] = do a2 <- f a1 x1
                               a3 <- f a2 x2
                               ...
                               f an xn

右から左への束縛は、foldM を呼ぶ前に入力リストを逆順に しておけばできます。

クローン羊の例のより強力な問い合せ関数を 作成するのに、foldM が使えます。

example3.hs で使えるコード
-- traceFamily は先祖を見つけるためのジェネリック関数です
traceFamily :: Sheep -> [ (Sheep -> Maybe Sheep) ] -> Maybe Sheep
traceFamily s l = foldM getParent s l
  where getParent s f = f s

-- traceFamily を使って、複雑な問い合せを、簡単に、明瞭に、定義できます
mothersPaternalGrandfather s = traceFamily s [mother, father, father]
paternalGrandmother s = traceFamily s [father, mother]

traceFamily 関数は foldM を使って、 家系図を任意パターンの任意の深さまで遡る、単純な方法を作りだせます。 実際、paternalGrandmother 関数を使うよりも明瞭に "traceFamily s [father, mother]"が書けます。

foldM のもっと典型的な使い方は、 do ブロックの中で使うとういものです。

example4.hs で使えるコード
-- Dict は文字列から文字列への単なる有限写像です
type Dict = FiniteMap String String

-- この補助関数は foldl で使います
addEntry :: Dict -> Entry -> Dict
addEntry d e = addToFM d (key e) (value e)

-- この補助関数は IO モナドの中で foldM とともに使います
addDataFromFile :: Dict -> Handle -> IO Dict
addDataFromFile dict hdl = do contents <- hGetContents hdl
                              entries  <- return (map read (lines contents))
                              return (foldl (addEntry) dict entries)

-- このプログラムは、コマンドラインから指定されたすべてのファイル中の
-- エントリから辞書を構築し、それを連想リストとして印字出力します
main :: IO ()
main = do files   <- getArgs
          handles <- mapM openForReading files
          dict    <- foldM addDataFromFile emptyFM handles
          print (fmToList dict)

filterM 関数はモナド内でリスト関数 filter と 同様の働きをします。この関数はモナド内で真偽値を返す述語と値のリストを 取ります。そして、モナド内で、述語が真になった値のリストを返します。

filterM :: Monad m => (a -> m Bool) -> [a] -> m [a]
filterM p []     = return []
filterM p (x:xs) = do b  <- p x
                      ys <- filterM p xs
                      return (if b then (x:ys) else ys)

これは filterM が IO モナド内で どのように使われて、リストからディレクトリのエントリだけを選択するかを 示した例です。

example5.hs で使えるコード
import Monad
import Directory
import System

-- 注意: doesDirectoryExist の型は FilePath -> IO Bool です

-- このプログラムはコマンドラインからディレクトリ名だけを印字します
main :: IO ()
main = do names <- getArgs
          dirs  <- filterM doesDirectoryExist names
          mapM_ putStrLn dirs

zipWithM はリスト上の zipWith 関数の モナド版です。zipWithM_ は、関数の結果を捨てる以外は 同じ振舞いをします。これはモナド計算の副作用のみが重要なときに便利です。

zipWithM ::(Monad m) => (a -> b -> m c) -> [a] -> [b] -> m [c]
zipWithM f xs ys = sequence (zipWith f xs ys)

zipWithM_ ::(Monad m) => (a -> b -> m c) -> [a] -> [b] -> m ()
zipWithM_ f xs ys = sequence_ (zipWith f xs ys)

条件式のモナド計算

条件によってモナド計算を実行するための関数がふたつあります。 when 関数は、真偽値とユニット"()"型のモナド計算を 取り、真偽値引数が True のときのみその計算を実行します。 unless 関数も同様ですが、こちらは、真偽値引数が True ではないかぎり 計算を実行します。

when :: (Monad m) => Bool -> m () -> m ()
when p s = if p then s else return ()

unless :: (Monad m) => Bool -> m () -> m ()
unless p s = when (not p) s

ap およびリフト関数

リフト は非モナド関数をモナド値上の同等の関数に変換する モナド演算です。リフト演算子により「モナドへリフトされた」関数、という 言い方をします。リフトされた関数は do ブロックの外側でモナド値を操作 するのに便利で、do ブロック内のコードをすっきりさせることができます。

最も単純なリフト演算子は liftM です。これは一引数関数を モナドにします。

liftM :: (Monad m) => (a -> b) -> (m a -> m b)
liftM f = \a -> do { a' <- a; return (f a') }

リフト演算子は多引数の関数用にも提供されています。 liftM2 は 2 引数関数をリフトします。

liftM2 :: (Monad m) => (a -> b -> c) -> (m a -> m b -> m c)
liftM2 f = \a b -> do { a' <- a; b' <- b; return (f a' b') }

おなじパターンが更に多引数の関数にも適用されています。 liftM5 までの関数が Monad モジュールで 定義されています。

リフト演算子がどのようにコードを簡潔にするのかを見るには、 Maybe モナド内の計算を考え、そこで swapNames::String -> String という関数を 使いたいとしましょう。すると、こんな風になるでしょう。

getName :: String -> Maybe String
getName name = do let db = [("John", "Smith, John"), ("Mike", "Caine, Michael")]
                  tempName <- lookup name db
                  return (swapNames tempName)

しかし、liftM 関数を使うと、 liftM swapNamesMaybe String -> Maybe String という型の 関数として使うことができます。

example6.hs で使えるコード
getName :: String -> Maybe String
getName name = do let db = [("John", "Smith, John"), ("Mike", "Caine, Michael")]
                  liftM swapNames (lookup name db)

この違いは多引数の関数をリフトすると更に大きくなります。

これらのリフト関数は高階関数をつかった非常に簡潔な構築を可能にします。 以下の例を理解するには、リスト モナド のモナド関数の定義(特に >>=) を復習する必要が あります。以下の関数をリフト演算子なしで実装することを想像してみましょう。

example7.hs で使えるコード
-- allCombinations は与えられたリストの要素のすべての組合せた結果
-- 全体を二項演算子で畳み込んだ結果を含むリストを返します。
-- たとえば、allCombinations (+) [[0,1],[1,2,3]] は、
--   [0+1,0+2,0+3,1+1,1+2,1+3]、あるいは [1,2,3,2,3,4] となり、
-- また、allCombinations (*) [[0,1],[1,2],[3,5]] は、
--   [0*1*3,0*1*5,0*2*3,0*2*5,1*1*3,1*1*5,1*2*3,1*2*5]、あるいは
-- [0,0,0,0,3,5,6,10] となります。
allCombinations :: (a -> a -> a) -> [[a]] -> [a]
allCombinations fn []     = []
allCombinations fn (l:ls) = foldl (liftM2 fn) l ls

関連する関数に ap とよばれているものがあります。 これは、場合によっては、lift 関数より便利なものです。 ap は単に関数適用演算子 ($) をモナドへリフトしたものです。

ap :: (Monad m) => m (a -> b) -> m a -> m b
ap = liftM2 ($)

liftM2 f x y は、 return f `ap` x `ap` y と 同等であり、それ以外の多引数関数についても同等であることに注意しましょう。 ap は高階関数とモナドをあつかっているときには便利です。

ap の作用はそこで使われているモナドの戦略に依存します。 それゆえ、たとえば、[(*2),(+3)] `ap` [0,1,2][0,2,4,3,4,5] に等しく、 (Just (*2)) `ap` (Just 3)Just 6 に等しくなります。 この例は単純なもので ap が、高階計算を行うときに どのように有用であるかを示したものです。

example8.hs で使えるコード
-- コマンドを検索し、ap をコマンドリストへ畳み込んで、結果を計算
main :: IO ()
main = do let fns  = [("double",(2*)),      ("halve",(`div`2)),
                      ("square",(\x->x*x)), ("negate", negate),
                      ("incr",(+1)),        ("decr",(+(-1)))
                     ]
          args <- getArgs
          let val  = read (args!!0)
              cmds = map ((flip lookup) fns) (words (args!!1))
          print $ foldl (flip ap) (Just val) cmds

MonadPlus で使うための関数

Monad モジュールにはゼロとプラスをもつモナドで使う ふたつの関数があります。最初の関数は、msum です。 これは、整数のリスト上の sum に似せたものです。 msum はモナド値のリスト上で操作をおこない、 mplus 演算子を、初期値として mzero 要素を使い、 そのリストに畳み込みます。

msum :: MonadPlus m => [m a] -> m a
msum xs = foldr mplus mzero xs

List モナドでは、msumconcat と同等です。 Maybe モナドでは msum はリストから最初の 非Nothing値を返します。同様に、他のモナドでの振舞いは まさにそれらモナドでの mzero および mplus の定義の性質に依存します。

msum は多くの再帰関数を可能にし、畳み込んでさらに表現を 簡潔にします。Maybe モナドでは、たとえば、

lookupVar :: Variable -> EnvironmentStack -> Maybe Value
lookupVar var []     = Nothing
lookupVar var (e:es) = let val = lookup var e
                       in maybe (lookupVar var es) Just val

と書くかわりに、

example9.hs で使えるコード
type Variable = String
type Value = String
type EnvironmentStack = [[(Variable,Value)]]

-- lookupVar は環境スタックから変数の値を検索します。
-- これは msum を Maybe モナド内で使い、最初の非-Nothing 値を返します
lookupVar :: Variable -> EnvironmentStack -> Maybe Value
lookupVar var stack = msum $ map (lookup var) stack

ゼロおよびプラスをもつモナドで使う、ふたつめの関数は guard 関数です。

guard :: MonadPlus m => Bool -> m ()
guard p = if p then return () else mzero

この関数を理解するコツは、ゼロおよびプラスをもつモナドに対する規則、 mzero >>= f == mzero を思い出すことです。 そうすると、guard 関数をモナド演算の並びの中に置くと、 すべてのその guard が False である実行が強制されて、 mzero になります。これは、リスト内包表記でガード述語が 述語を失敗させ、[] になる値を生む方法と同様です。

次の例は Maybe モナドでの guard 関数の 使い方を例示するものです。

example10.hs で使えるコード
data Record = Rec {name::String, age::Int} deriving Show
type DB = [Record]

-- getYoungerThan は指定した年齢より若い人の、全てのレコードを返します。
-- これはガード関数を用いて、制限値およびそれを越える年齢の人のレコード
-- を消去するのに使います。これは例示にすぎません。実際には単に filter
-- を使う方が明解です。filter の許容値がもっと複雑な場合には、guard が
-- もっと役に立つでしょう。
getYoungerThan :: Int -> DB -> [Record]
getYoungerThan limit db = mapMaybe (\r -> do { guard (age r < limit); return r }) db

要約

Haskell では標準ライブラリでモナド用の便利な関数をいくつも提供しています。 Monad クラスとほとんどの共通のモナド関数は標準プレリュード にあります。MonadPlus クラスはそれほど頻繁に使われるもの ではない(とはいえ非常に有用な)関数は Monad モジュールで定義 されています。Haskell ライブラリで、多くの他の型は Monad および MonadPlus クラスのインスタンスとしてそれぞれの モジュールで宣言されています。


Prev: 練習問題 TOC: 目次 Next: Part II - イントロダクション