Haskell: 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
[RAW]
  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
[RAW]
  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 ()型なので勘違い?

(2020.5) モナドが何であるか、どのような制約を満たすか、などについては、ページを分けました; モナド, 具象データ型, モナド則の嬉しさ

失敗

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

Haskell
[RAW]
  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となる。

モナドのすべて

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