Prev: 練習問題 | TOC: 目次 | Next: Part II - イントロダクション |
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 |
標準の 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
関数は、標準プレリュードでエクスポートされています。
その内容は前述のとおりです。
foldM
は foldl
のモナド版で、リストから構築
されたモナド計算は左から右へ束縛されます。その定義は、
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 swapNames
を
Maybe 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 モナドでは、msum
は concat
と同等です。
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 - イントロダクション |