モナド
このページでは,Haskell を理解するうえでカギとなる概念である,モナドについて説明します.
まず最初に,モナドを自然に理解できるように,モナドと近い概念である "ファンクタ" を紹介します.
その次に,モナドを紹介します.
それから,モナドの例として Maybe
モナドを紹介し,モナドのための構文糖である do
記法を説明します.
1. ファンクタ
モナドを自然に理解できるように,はじめに ファンクタ(functor)とよばれる型クラスを紹介します.
モナドと同様,ファンクタも圏論に由来する概念のひとつです.しばしば "関手" とも訳されます. |
リストの要素を操作する関数 map
を一般化した fmap
という関数があります.
ファンクタはこの fmap
をメンバとする型クラスであり,次のように定義されています.
class Functor t where
fmap :: (a -> b) -> t a -> t b
Functor
クラスは,値を入れるコンテナみたいな構造をもつ型を表します.
別の言い方をすれば,なんらかの型をラッピングした(包み込んだ)型を表します.
インスタンスには,Maybe
,[]
(リスト),IO
などがあります.
関数 fmap
は,ラッピングされた値に対する操作を可能にする関数です.
いくつか使用例を見てみましょう.
f x = 2 * x
main = do print $ fmap f (Just 5) -- 出力: Just 10
print $ fmap f Nothing -- 出力: Nothing
print $ fmap f [1, 2, 3] -- 出力: [2, 4, 6]
print $ fmap f [] -- 出力: []
Maybe
および []
のインスタンス宣言は,次のようになっています.
instance Functor Maybe where
-- fmap :: (a -> b) -> Maybe a -> Maybe b
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
instance Functor [] where
-- fmap :: (a -> b) -> [a] -> [b]
fmap = map
Functor クラスのインスタンスはあくまで Maybe や [] であって,Maybe a や [a] ではないことに注意します.
一般に,Functor クラスのインスタンスは,具体型を 1 つとる型構成子です.
|
数学的には,ファンクタは次の 2 つの規則(ファンクタ則)を満たすことが要求されます.
実際, |
関数 f
の型が f :: a -> b
のとき,fmap f
の型は fmap f :: t a -> t b
です.
この関係を図にすると,次のようになります.
関数 fmap
によって,普通の関数 f
が,(Maybe
や []
で)ラッピングされた値に作用する関数 fmap f
に変換されている様子がよくわかります.
こうした変換のことを,関数の持ち上げ(lifting)ということがあります.
2. モナドの定義
モナドもファンクタと同じく,なんらかの型をラッピングした型を表す型クラスです. ファンクタで見たさっきの図の矢印の向きを少し変えると,モナドを説明した図ができあがります.
この図の $m$ がモナドのインスタンス,演算 $(\cdot)^*$ がモナド版の持ち上げ演算です.
なお, $f^*$ というのは数学上の表記であり,実際の Haskell プログラムではこれを (>>= f)
と書くことになります.
ちなみに,演算 $(\cdot)^*$ は クライスリスター(Kleisli star)と呼ばれます. |
モナドの型クラスは次のように定義され,基本的な演算として >>=
と return
を持ちます.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
(>>) :: m a -> m b -> m b
m >> n = m >>= \_ -> n
fail :: String -> m a
fail = error
ファンクタのときと同じく,数学的には,モナドは次の 3 つの規則(モナド則)を満たさなければなりません.
|
演算 >>=
は バインド(bind)演算と呼ばれる左結合演算子で,Functor
クラスの fmap
と同様に関数を持ち上げます.
演算 return
は,普通の値をラッピングされた値にする関数です(C/C++ とかの return
文とは関係ありません).
その他の演算 >>
と fail
はあまり重要ではありません.
演算 >>=
と return
の関係を図にすると,次のようになります.
演算 >>=
と return
は,例えば次のように使われます.
f x = return (2 * x)
main = do print $ Just 5 >>= f -- 出力: Just 10
print $ Nothing >>= f -- 出力: Nothing
print $ [1, 2, 3] >>= f -- 出力: [2, 4, 6]
print $ [] >>= f -- 出力: []
ファンクタを理解できていれば,モナドも自然に理解できますね(少なくとも Maybe
モナドみたいな簡単なモナドなら).
Haskell で定義されている代表的なモナドには,次のようなものがあります.
-
Identity
-
Maybe
-
Error
-
[]
(リスト) -
IO
-
State
-
ST
-
Reader
-
Writer
ご存じのように,この中の IO
モナドが,現実世界とのやりとり(入出力処理)に使われるモナドです.
入出力の値を IO
モナドというベールで隠蔽してしまうことにより,"(参照透過性はそのままで)副作用のある処理を行う" という側面を実現しています.
3. Maybe モナド
定義が簡単な Maybe
モナドを例にとって,バインド演算の動きを追ってみます.
Maybe
モナドのインスタンス宣言は次のように書かれます.
instance Monad Maybe where
Nothing >>= f = Nothing
Just x >>= f = f x
return x = Just x
fail _ = Nothing
Maybe
モナドのバインド演算は,失敗しうる計算を連鎖させて,失敗しうる大きな計算を作ります.
次のプログラムはその好例です.
-- x が 2 で割り切れない場合には失敗する
div2 :: Int -> Maybe Int
div2 x = if even x then Just (x `div` 2)
else Nothing
-- x が 8 で割り切れない場合には失敗する
div8 :: Int -> Maybe Int
div8 x = return x >>= div2 >>= div2 >>= div2
main = do print $ div8 32 -- 出力: Just 4
print $ div8 50 -- 出力: Nothing
バインド演算の定義を展開して書くと,関数 div8
は次のように書けます.
div8 :: Int -> Maybe Int
div8 x =
case return x of
Nothing -> Nothing
Just y ->
case div2 y of
Nothing -> Nothing
Just z ->
case div2 z of
Nothing -> Nothing
Just w -> div2 w
これを見て一目瞭然なように,バインド演算は左から右へ順番に式を評価する性質をもちます.
IO
モナドでも同じ理屈で,"(遅延評価はそのままで)処理を逐次的に実行する" という仕組みが実現されています.
4. do 記法
モナドのバインド演算の構文糖として,do
式(do
記法)が利用できます.
この do
式の存在が,モナドを Haskell において特別な存在にしています.
do { 式1 ; ...; 式n }
do 式の中に書ける式は,次の 3 通りです.
(a) モナド式
(すなわち,Monad m => m a
型の式)
(b) パターン <- モナド式
(c) let { 宣言1 ; … ; 宣言n }
式 (b) は,右辺が m a
型であれば,a
型の値を左辺のパターンに束縛します.
式 (c) は,普通の変数や補助関数を宣言します.
式 (b),(c) で宣言された名前は,その do
式の中だけで有効です.
do
式の中の最後の式は (a) でなければなりません.
この式の値が,do
式全体の値となります.
先の div8
関数は,do
式を用いると次のように書けます.
div8 :: Int -> Maybe Int
div8 x = do y <- div2 x
z <- div2 y
div2 z
すこし別の例を見てみましょう. 次のプログラムは,入力された文字列を 1 行ごとに大文字に変換して出力するものです. 空行が入力された時に終了します.
import Data.Char
main :: IO ()
main = do s <- getLine
let t = map toUpper s
if null s then return ()
else do { putStrLn t ; main }
このプログラムの
|
実行例は次のようになります($
はプロンプト,太字は入力した文字列).
$ runghc Sample.hs hello HELLO hello, Haskell HELLO, HASKELL $