Haskellerへの道 #15 - Monad

Last Edited: 6/28/2024

このブログ記事では、Haskellの非常に重要な型クラス、Monadを紹介します。

Haskell & Monad

振り返り

前回の2つの記事では、FunctorApplicativeについて取り上げました。Functorは、型コンストラクタ内の値に対してfmapを使って関数を実行でき、 Applicativepure<$>、および<*>を使って一つのファンクター内の関数を別のファンクター内の値に対して実行できます。 FunctorApplicative Functorについて自信がない場合は、Road to Haskeller #13と#14の記事を確認してください。

Monad

さて、いよいよMonadについて話す準備が整いました。MonadApplicativeの自然な進化形であり、型aから型bを内包するApplicativeを 導出する関数を型aを内包するApplicativeに使用できるようにしたものです。言葉ではよくわからないと思うので早速、Monadの定義を見てみましょう。

class Applicative m => 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 (>>=) #-}

MINIMALからわかるように、ApplicativeMonadになるためには(>>=)またはbindを定義するだけで済みます。 bind関数は型aの値を持つApplicativeと、型aの入力を取り、型bのApplicativeを出力する関数を渡して、型bの値を持つApplicativeを得る関数です。 これはFunctorApplicativeでは不可能でした。なぜなら、Functor(a -> b)の関数しか受け付けず、 Applicativeはファンクター内の(a -> b)しか受け付けないからです(bindは (a -> m b)を使用可能)。 Monadをより理解するために、いくつかの例を見てみましょう。

Maybe

Maybeは次のように定義されたMonadのインスタンスです。

instance Monad Maybe where
    return x = Just x
    Nothing >>= f = Nothing
    Just x >>= f  = f x
    fail _ = Nothing

return関数はpureと同じで、値をJustに内包します。bind関数または>>=は、Nothingが渡された場合はNothingを出力し、 それ以外の場合はJust内の値に対して関数を適用した結果を出力します。これは関数f内でMonadを使うことができるため、非常に便利です。

Applicativeの場合、入力の1つがNothingでなければNothingを得ることができませんでした。 これは、関数の出力がMaybe a内の型,aでなければならず、Maybe aそのものであってはいけなかったからです。 それに対しMonadでは、型aの値を取り、Maybe aを返す関数を持てるようになったので、次のようなことができるようになりました。

safediv :: (Eq a, Fractional a) => a -> a -> Maybe a
safediv x y
  | y == 0 = Nothing
  | otherwise = Just (x / y)
 
Just 1 >>= (\x -> safediv x 10) --- (Just 0.1)
Just 1 >>= (\x -> safediv x 0) --- (Nothing)

(a -> a -> m a)safedivMaybe内の値に適用することはできるようになったことにより、 Nothingを返す条件をさらに柔軟に設定できるようになり、Nothingが渡された場合以外にも操作中に失敗を返すことができるようになりました (y == 0の時にNothingを出力できるようになった)。 これにより、エラーを引き起こしにくい関数を簡単に作成できます。

リスト

リストもMonadの有効なインスタンスであり、次のように定義されています。

instance Monad [] where
    return x = [x]
    xs >>= f = concat (map f xs)
    fail _ = []

return関数はpure関数と同じで、値をリストに入れます。bind関数はリスト内の要素に関数をマッピングし、その結果を連結してリストにします。 次の例はリストのbind関数がどのように機能するかを示しています。

[3,4,5] >>= \x -> [x,-x]
--- [3,-3,4,-4,5,-5]

匿名関数\x -> [x, -x]はリスト内の各要素に適用され、その結果[3, -3][4, -4]、および[5, -5]が連結されて最終的なリストが作成されます。 Maybeとリストの他にも、Monad型クラスに属する多くの型コンストラクタが存在し、その中にはIOも含まれます。

Do構文

bind関数を使用して、safedivのような複数のパラメータを持つ関数を使用する場合、次のように実現できます。

res = Just 1 >>= (\x -> Just 2 >>= (\y -> safediv x y))

しかし、これは書くのも読むのも簡単ではありません。そこで、Haskellはより良い構文を用意してくれています。

res = do
    x <- Just 1 --- x = 1
    y <- Just 2 --- y = 2
    return $ safediv x y --- Just 0.5

おや、この記法はどこかで見たことがありませんか?そうです、これはRoad to Haskeller #12 - Hello, World (IOアクション)!で見た記法です。 <-は実際には>>=(どちらもbindと呼ばれています!)と同じもので、doは括弧を省略するためのものです。 気づいた方もいるかもしれませんが、<-記法は他の場所でも見たことがあります。それはRoad to Haskeller #3 - リスト & タプルでリスト内包表記を学んだときです!

--- リスト内包表記
res = [2*x | x <- [1,2,3]] --- res = [2,4,6]
 
--- Do構文
res = do
    x <- [1,2,3]
    return $ 2 * x --- res = [2,4,6]
 
--- <<=記法
res = [1,2,3] >>= (\x -> return $ 2 * x) --- res = [2,4,6]

Monad則

型コンストラクタが正しいMonadであるためには以下の法則を遵守する必要があります。

  • 左単位元: 値をMonadに入れて、bindを使って関数を適用することは、値を直接関数に渡した場合と同じ結果になる必要があります。 (return x >>= f == f x)

  • 右単位元: Monad内の値をbindを使ってreturnに渡した場合、同じ値を持つ同じモナドが結果でなければなりません。 (m >>= return == m)

  • 結合性: bindで作られた関数の連鎖がどれほどネストされていても、結果は同じである必要があります。 (m >>= f >>= g == (m >>= f) >>= g)

クイズ

この記事では、学習した内容を確認するためのクイズを設けます。記事のメイン部分を読んだ後に、ぜひ自分で問題を解いてみることを強くお勧めします。各問題をクリックすると答えが表示されます。

リソース