モナド

2024/07/17

数学

モナドとは

圏論(数学の一分野)におけるモナドは、自己関手 TT、単位 ηη、乗法 μという3つの要素と、それらが満たすべき結合律・単位律からなる数学的構造であり、モノイドに似た構造を備えた自己関手である。プログラミングにおけるモナドは、圏論におけるモナドを応用し、値を包み込んで付加的な情報を意識せず簡単に変換できるようにするための統一的なインターフェース、あるいはデザインパターンである。圏論におけるモナドにはやや難解な点もあるが、プログラミングにおけるモナドは副作用やエラーといった文脈付きの計算を安全に扱うするための道具(デザインパターン)という理解で問題ない。

圏論におけるモナド

圏論の基礎:対象、射、関手、自然変換

圏論におけるモナドを説明するため、まず圏論の基礎について簡単に説明する。とは、対象(矢印)の集合であり、射は対象間の関係を表す。ある圏から別の圏への構造を保つ写像を関手と呼び、関手は対象を対象に、射を射に対応させ、恒等射と射の合成を保存する。特に、ある圏からそれ自身への関手を自己関手と呼ぶ。また、関手間を自然に(自然性を満たすように)結ぶ変換を自然変換と呼ぶ。自然変換は2つの関手間に対応する全ての対象に対して射を対応させ、その対応が「整合性条件(可換図式)」を満たすものである。

プログラミングに関連付けてイメージするなら、対象を「型」、射を「関数」と考えると分かりやすい。このとき、関手は型と関数を別の文脈に持ち込む仕組みとして現れる。例えば、リスト内の各要素に関数を適用して新しいリストを返すfmapなどが典型的な自己関手である。また、自然変換は2つの関手をつなぐプログラム上の汎用的な変換として現れる。

圏論におけるモナドの定義

モナドは圏CC上の自己関手T:CCT:C\to Cと2つの自然変換η:idT\eta:id{\Rightarrow}Tμ:T2T\mu:T^2{\Rightarrow}Tの組(T,η,μ)(T, \eta, \mu)として定義される。自然変換η\etaをモナド(T,η,μ)(T, \eta, \mu)の単位元、自然変換μ\muをモナド(T,η,μ)(T, \eta, \mu)の乗法を呼ぶ。T,η,μT, \eta, \muについての簡単な説明を次に示す。

  • 自己関手T:CCT:C\to Cは圏CCの対象や射に何らかの文脈を付加する役割を果たす
  • 自然変換η:idT\eta:id{\Rightarrow}T(単位)は 、各対象XXに対して射ηX:XT(X)\eta_X:X{\to}T(X)を対応させ、任意の値をモナドの文脈に持ち上げる関数の役割を果たす
  • 自然変換μ:T2T\mu:T^2{\Rightarrow}T(乗法)は、各対象XXに対して射μX:T(T(X))T(X)\mu_X:T(T(X)){\to}T(X)を対応させ、二重になった文脈を一重に潰す(flatten)関数の役割を果たす
  • Notion Image

    なお、T,η,μT, \eta, \muは次に示すモナド則を満たす必要がある。

  • 結合律:μXT(μX)=μXμT(X)\mu_X{\circ}T(\mu_X)=\mu_X{\circ}\mu_{T(X)}
  • 左単位律:μXηT(X)=idT(X)\mu_X{\circ}\eta_{T(X)}=id_{T(X)}
  • 右単位律:μXT(ηX)=idT(X)\mu_X{\circ}T(\eta_X)=id_{T(X)}
  • 結合律は複数の計算ステップをどの順序で結合しても結果が一つに定まることが保証し、単位律はη\etaμ\muに関して単位元のように振る舞うことを示す。

    モナド則は可換図式で表現すると次のようになる。

    Notion Image

    ※補足:T(μX)T(\mu_X)μT(X)\mu_{T(X)}T(ηX)T(\eta_X)ηT(X)\eta_{T(X)}の違い

    Notion Image

    (1)T(μX)T(\mu_X)μX:T(T(X))T(X)\mu_X:T(T(X))\to T(X)を関手TTで移したもの

    (2)μT(X)\mu_{T(X)}T(X)T(X)を一塊とした自然変換μ\muT(X)T(X)成分、すなわち、T(T(X))T(X)T(T(X))\to T(X)XXT(X)T(X)で置き換えたもの

    (3)T(ηX)T(\eta_X)ηX:XT(X)\eta_X:X\to T(X)を関手TTで移したもの

    (4)ηT(X)\eta_{T(X)}T(X)T(X)を一塊とした自然変換η\etaT(X)T(X)成分、すなわち、XT(X)X\to T(X)XXT(X)T(X)で置き換えたもの

    モナドは「自己関手の圏におけるモノイド対象」?

    そもそもモノイドとは、集合MMとその上の結合的な二項演算 :M×MM{\cdot}:M{\times}M{\rightarrow}M 、その演算に関する単位元eMe{\in}Mの組(M,,e)(M,\cdot,e)のことである。

    ここで、ある圏 C 上の自己関手を対象とする圏を考える。この圏の対象は自己関手で、射は自己関手間の自然変換である。この圏では関手の合成が積のように振る舞い、恒等関手ididが単位元のように振る舞う。この圏の中でモノイドのような構造を持つも対象を考えると、それがモナドになる。自己関手TTがモノイドの集合MMに、乗法μ\muが二項演算 \cdot に、単位η\etaが単位元eeに対応する。このことは、モナド則が自己関手の圏におけるモノイドとしての構造に由来していて、代数学における基本的な構造であるモノイドの法則を関手と自然変換の世界に翻訳したものであることを示唆している。

    プログラミングにおけるモナド

    プログラミングにおけるモナドの定義

    モナドは通常、特定のインターフェースや型クラスとして定義され、共通の操作が提供される。具体的には、モナドの型クラスは次のように定義され、 >>=と return の2つの操作をもつ。

    class (Applicative m) => Monad m where
      (>>=) :: m a -> (a -> m b) -> m b
      return :: a -> m a

     >>=と return は、次のモナド則を満たす。

    (return x >>= f) == f x  -- 左単位則
    (m >>= return) == m  -- 右単位則
    ((m >>= f) >>= g) == (m >>= (\x -> f x >>= g))  -- 結合則

    >>=はバインド(bind)演算と呼ばれる左結合演算子で関数をモナドの文脈に持ち上げる。returnは素の値をモナドに包まれた値にする。演算 >>=と return の関係を図にすると次のようになる。

    Notion Image

    様々なモナド

    ① Maybeモナド

    Maybeモナドは失敗する可能性のある計算をうまく扱えるようにしたモナドで、成功時にはJust xを、失敗時にはNothingを返す。これにより、途中で失敗した場合はそれ以降の処理を自動的にスキップできるため、複数の計算を組み合わせた処理もシンプルに記述できる。Maybeモナドのインスタンスは次のように定義される。

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

    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 :: IO ()
    main = do
      print $ div8 32  -- 出力: Just 4
      print $ div8 50  -- 出力: Nothing

    ② Eitherモナド

    Eitherモナドはエラー情報を伴う計算をシンプルに扱うためのモナドで、成功時には Right x を、失敗時には Left eeはエラー情報)を返す。エラー情報を保持しつつ計算を連結できるため、複雑になりがちなエラー処理もシンプルに記述できる。Eitherモナドのインスタンスは次のように定義される。

    instance Monad (Either e) where
      (Right x) >>= f = f x
      (Left e)  >>= _ = Left e
      return x = Right x

    Eitherモナドの使用例を以下に示す。

    -- x が 2 で割り切れない場合にはエラーメッセージを返す
    div2 :: Int -> Either String Int
    div2 x =
      if even x
        then Right (x `div` 2)
        else Left  ("Cannot divide " ++ show x ++ " by 2")
    
    -- x が 8 で割り切れない場合には失敗する
    div8 :: Int -> Either String Int
    div8 x = return x >>= div2 >>= div2 >>= div2
    
    main :: IO ()
    main = do
      print $ div8 32   -- 出力: Right 4
      print $ div8 50   -- 出力: Left "Cannot divide 25 by 2"

    ③ Listモナド

    Listモナドは、非決定的な計算(複数の値を持つ計算)を扱うモナドで、リストの各要素に対して総当たり的に計算を行う。Listモナドのインスタンスは次のように定義される。

    instance Monad [] where
      xs >>= f = concatMap f xs
      return x = [x]

    Listモナドの使用例を以下に示す。

    -- リストモナドを使って各要素を2倍にする関数
    doubleList :: [Int] -> [Int]
    doubleList xs = do
      x <- xs
      return (x * 2)
    
    main :: IO ()
    main = print $ doubleList [1,2,3,4]  -- 出力: [2,4,6,8]

    関数型プログラミングでモナドを使う理由

    モナドは、値に付随する“文脈”を、意識せずに安全かつシンプルにつなげるためのデザインパターンである。モナドを利用することで次のような恩恵がある。

  • コードの明確性 例外処理やエラーハンドリングをモナドの内部に隠蔽することで分岐を減らし、コードを直線的に記述できる。
  • 合成可能性 異なる種類の計算(純粋関数、失敗する可能性のある計算、非同期計算など)を、>>= (bind)で組み合わせることを可能にする 。
  • 安全性 純粋関数(Int → Intなど)と、外部アクセスを伴う処理(IO Intなど)を型レベルで区別でき、安全に組み合わせられる。
  • 参考資料



    著者画像

    ゆうき

    2018/04からITエンジニアとして活動、2021/11から独立。主な使用言語はPython, TypeScript, SAS, etc.