背景

新くりかえしモナドドリル を解いてみました。これは Haskell のモナドと do 記法に関する小さな問題集です。タイトル通り、夏休みの宿題みたいな見た目をしています。

2025-08-23-monad-drill.jpg

do 記法を理解する

本書では、主に do 記法の desugaring の知識が問われているように思いました。以下の背景知識があれば、バッチリ解けると思います。

let の展開

do 記法の中では let 式を文のように書くことができます:

let x = do
      let y = 10
      let z = 20
      y + z

これは let .. in .. 式に展開されます:

let x =
      let y = 10
       in let z = 20
           in y + z

なお例の通り、式全体がモナドではない場合にも do 記法を使うことができます。

操作の連結 (>>)

do 記法の中でモナド値 (monadic action) を連結した場合、 (>>) 演算子によって接続されます:

let x = do
      Just 2
      Just 3
      Just 4
let x = ((Just 2 >> Just 3) >> Just 4)

(>>)(>>=) を使って定義されており、左辺の出力を捨てて右辺を評価します:

(>>)        :: forall a b. m a -> m b -> m b
m >> k = m >>= \_ -> k

右辺の評価について

モナドの値に応じて、 (>>=) 内で右辺の値が捨てられる場合もあります:

-- | @since base-4.4.0.0
instance Monad (Either e) where
    Left  l >>= _ = Left l
    Right r >>= k = k r

したがって LeftNothing が見つかった時点で、 do 記法内の後の式は評価されないことが確定します。

両辺の型について

(>>)(>>=) の両辺の間は、あくまで文脈 (モナド) の型が合っていれば良く、異なる型の値を引数に取ることができます。たとえば次の式はコンパイル可能です:

Right "42" >> Right (42 :: Int)

ただ Either に関しては、 Either e に対して Monad が実装されており、失敗にあたる部分まで型が一致しなければ >>, >>= を適用できません。だから Either a bLeft a の方が失敗を表すのですね。

Left "42" >> Left (42 :: Int)

Bind (<-) の展開

最後はお馴染みの bind (<-) を使った do 記法です:

let x = do
      y <- Just (1 :: Int)
      z <- Just (2 :: Int)
      pure $ y + z

これはもちろん、ネストした (>>=) に展開されます:

let x =
      Just (1 :: Int)
        >>= ( \y ->
                Just (2 :: Int)
                  >>= ( \z ->
                          pure $ y + z
                      )
            )

(>>) は単純な展開でしたが、 (>>=) はネストした式になる点は要注意です。

(>>=) の中では、短絡評価を実施したり、引数を状態とみなしたり、 concatMap するなど、モナド毎に様々なトリックが実装されます。

MonadFail

Bind の左辺でパタンマッチに失敗した場合は fail 関数 の値に fallback します:

let x = do
      Just y <- Nothing
      pure 42
-- x == Nothing になる

これも do 記法においては desugaring されます:

let x =
      let f (Just y) = pure y
          f _ = fail "<compiler-generated message>"
       in f Nothing

Maybe においては fail _ = Nothing です:

instance MonadFail Maybe where
    fail _ = Nothing

まとめ

モナドドリルを通し、 Haskell の do 記法がどのように解釈されるか再確認できました。操作の連結 (a; b; c) と bind (<-) の展開において、異なる desugaring が実施されることが明確に認識できました。また MonadEither e に対して実装されている点が面白かったです。

参考