do記法

意外と制限があって難しい, do記法について。

構文

Haskell 2010 で, 構文は次のようになっている。[ ] は省略可, | はどちらか.

lexp
do { stmts }
stmts
stmt1 ... stmtn exp [;]             (n ≧ 0)
stmt
exp ;
  | pat <- exp ;
  | let decls ;
  | ;

ただし, expは式, patは変数やパタンマッチ, declsは宣言。let文がいくつも書けることが分かる。

上記の {}, ; については、レイアウトルール (off-sideルール) により、字下げしたときは省略できる。普通は省略するように字下げする。

doの脱糖

Haskell のdo記法は構文糖 (syntactic sugar). 理解のために, 脱糖 (desugar) したときどうなるか, を見てみる。

次のプログラムは,

Haskell
[POPUP]
  1. import System.Environment
  2. main :: IO ()
  3. main = do
  4. args <- getArgs
  5. case args of
  6. [] -> putStrLn "empty!!"
  7. x:_ -> putStrLn x -- 単に head argsだと, 純粋なのに例外発生
  8. putStrLn "END"

次のように書き換わる;

Haskell
[POPUP]
  1. import System.Environment
  2. main :: IO ()
  3. main = getArgs >>=
  4. \args -> (case args of
  5. [] -> putStrLn "empty!!"
  6. x:_ -> putStrLn x) >>
  7. putStrLn "END"

前から順に >> 演算子か >>= 演算子で繋ぐ形に書き換えられる。(>>)をthen演算子, (>>=)をbind演算子という。いずれも、左側を実行してから右側を実行する演算子。これで順次実行ができる。

>>= のほうは、ラムダ式で受ける形。

厳密な書き換えルールは次のようになっている。これも Haskell 2010 から;

1. do { e } = e
2. do { e ; stmts } = e >> do { stmts }
3. do { pat <- e ; stmts } =
let ok pat = do { stmts }
ok _ = fail "..."
   in e >>= ok
4. do { let decls; stmts } = let decls in do { stmts }

書き換えルール3のpatは変数とは限らず、パタン。パタンマッチの失敗 (fail) があるので、このようになっている。失敗については後述。

ルール4から, let文は書いたところから後ろにしか効果がないことが分かる。

モナド

次に、do記法内の式として, どんな型の式でも書けるのかどうか。

結論から言えば, 値の型は, 型クラス Monad を実装するデータ型でなければならない。インターネット上の解説で, IO a 型でなければならない、というのがあるが、誤り。もちろん IO も書けるが, IO限定ではない。main関数が IO ()型なので勘違い?

型クラス Monad の宣言とデフォルト実装は、次のようになっている. ここで (>>=)(>>)が宣言されている。

Haskell
[POPUP]
  1. class Applicative m => Monad (m :: * -> *) where
  2. (>>=) :: m a -> (a -> m b) -> m b
  3. (>>) :: m a -> m b -> m b
  4. return :: a -> m a
  5. fail :: String -> m a
  6. m >> k = m >>= \_ -> k
  7. fail s = error s
(>>) :: m a -> m b -> m b
左辺と右辺でデータ型名の部分 (例えば IO) は同じだが, 型変数の部分は替わってもよい。
(>>=) :: m a -> (a -> m b) -> m b
右辺の関数の第1引数、つまり pat <- exp と書くときのpat の型が m aでないことに注意。値の世界では, コンストラクタを書かない, ということ。
return
ほかのプログラミング言語のような呼び出した関数から抜けるという意味ではない。式 (文) の値がモナドではないときに, モナド化する。

Monadを実装する各データ型は, Applicative, Functor も実装している。

型クラス Applicative

Haskell
[POPUP]
  1. class Functor f => Applicative (f :: * -> *) where
  2. pure :: a -> f a
  3. (<*>) :: f (a -> b) -> f a -> f b
  4. (*>) :: f a -> f b -> f b
  5. (<*) :: f a -> f b -> f a

型クラス Functor

日本語だと関手。

Haskell
[POPUP]
  1. class Functor (f :: * -> *) where
  2. fmap :: (a -> b) -> f a -> f b
  3. (<$) :: a -> f b -> f a

失敗

パタンマッチの失敗は、例外とは違う。しかし、doの書き換えルールによって, do式内の後続の式 (文) は実行されない。

Haskell
[POPUP]
  1. -- インデックスを与えて, リストを得る
  2. mylookup :: Int -> Maybe [Int]
  3. mylookup index = do
  4. let list = [Just [1, 2, 3], Nothing, Just [], Just [7..10]]
  5. (_:xs) <- list !! index -- 左辺は Just (_:xs) ではない
  6. -- index が 1 or 2のとき、左辺がマッチしない
  7. return xs -- [Int] 型を Maybe [Int] 型にする
  8. put_mylookup :: Int -> IO ()
  9. put_mylookup index = do
  10. case mylookup index of
  11. Just x -> putStrLn $ show x
  12. Nothing -> putStrLn "nothing"
  13. main :: IO ()
  14. main = do
  15. put_mylookup 0 -- [2,3]
  16. put_mylookup 1 -- nothing
  17. put_mylookup 2 -- nothing
  18. put_mylookup 3 -- nothing
  19. put_mylookup 4 -- (!!)で例外発生

実行結果;

[2,3]
nothing
nothing
[8,9,10]
fail: Prelude.!!: index too large

なぜこのようになるのか。Maybeは, 型クラスMonadの実装として, 次のように定義されている。

instance Monad Maybe where
    (Just x) >>= k   = k x
    Nothing  >>= k   = Nothing
    return           = Just
    fail _           = Nothing

行5 で、indexが1のときは, パタンマッチ以前に Nothing >>= k = Nothingで, 値は Nothing となる。

index が 2のときには, []なので, 書き換えルール3でいう patにマッチしない。ok _のほうから failが呼び出される。この値は Nothing.

結局、do式の値は Nothingとなる。

モナドのすべて

モナド変換子などについても。