はじめに
皆様こんにちは、わんこと申します。
先日開催された関数型まつりに参加した際、色んな人がモナドについて語っている中、自分にはモナドの基本的な知識がなく悔しい思いをしました。
そのためモナドについて基本的なことを色々と調べてみたので、その内容についてまとめます。
前提
モナドという概念は元々、圏論という数学の分野にて定義されました。
そしてこのモナドが関数プログラミングに輸入されたことで、関数プログラミングの文脈でも利用されるものとなりました。
そのため、モナドを語るときには以下の3段階の抽象度が考えられます。
- 圏論のモナド
- 関数プログラミング全般のモナド
- 特定の関数言語のモナド
本記事は主に2番以降のプログラミング文脈でのモナドについて解説していきます。
モナドとは
モナドとは一体何なのでしょうか?
今回は各言語におけるモナドの定義を見ながらその詳細を探っていきたいと思います。
Haskellのモナド (Control.Monad)
まずはHaskellにおけるモナドの定義を見てみましょう。
Haskellでは型クラスというものでモナドが定義されています。
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
return = pure -- return と pure は同じ
class Functor f => Applicative f where
pure :: a -> f a
class Functor f where
fmap :: (a -> b) -> (f a -> f b)
1つ目の(>>=)は一般的にbindと呼ばれる関数であり、モナドの文脈を持つ値m aを受け取り、モナドの文脈の中にあるaを引数として渡された関数に適用して、その結果を返却します。
2つ目のreturnは引数aにモナドの計算効果を付与して返す関数です。これはApplicative型クラスに定義されたpureをデフォルト実装として持っており、同一視することができます。
また継承元であるFunctor型クラスにa -> bという関数をf a -> f bに変換する関数であるfmapが定義されています。
Scalaのモナド (cats.Monad)
次にScalaのモナドを見てみましょう。
trait Monad[F[_]] extends FlatMap[F] with Applicative[F] {
// 省略
}
trait FlatMap[F[_]] extends Apply[F] with FlatMapArityFunctions[F] {
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}
trait Applicative[F[_]] extends Apply[F] with InvariantMonoidal[F] { self =>
def pure[A](x: A): F[A]
}
trait Apply[F[_]] extends Functor[F] with InvariantSemigroupal[F] with ApplyArityFunctions[F] { self =>
// 省略
}
trait Functor[F[_]] extends Invariant[F] { self =>
def map[A, B](fa: F[A])(f: A => B): F[B]
}
こちらもApplicativeを継承する形でモナドが定義されています。
ScalaではHaskellにおけるbindはflatMap、returnはpure、fmapはmapとして関数が実装されています。
Clojureのモナド (algo.monads)
では変わり種として、動的型付け言語であるClojureのモナドを見てみましょう。
(defmacro monad
[operations]
`(let [~'m-bind ::this-monad-does-not-define-m-bind
~'m-result ::this-monad-does-not-define-m-result
~'m-zero ::this-monad-does-not-define-m-zero
~'m-plus ::this-monad-does-not-define-m-plus
~@operations]
{:m-result ~'m-result
:m-bind ~'m-bind
:m-zero ~'m-zero
:m-plus ~'m-plus}))
(defmonadfn m-fmap
"Bind the monadic value m to the function returning (f x) for argument x"
[f m]
(m-bind m (fn [x] (m-result (f x)))))
Clojureではモナドをマクロを用いて表現しています。
こちらではm-bind, m-fmapに加え、returnはm-resultという関数で定義されています。
結局モナドってなに?
ここまで色々な言語のモナドの定義を見てきました。
各言語のモナド定義を見ると、少なくともbind (flatMap)とpure (return), map (fmap)という操作を行うことができることは共通要素のようです。
これはpureとbindが後述するモナド則を満たすとき、pure, bind, fmapは圏論的なモナドの定義に現れる構造と対応づけることができるためです。
- 自己関手 $T : \mathcal{C} \to \mathcal{C}$
- 自然変換 $\eta : \mathrm{Id}_\mathbf{C} \Rightarrow T$
- 自然変換 $\mu : \mathrm{T^2} \Rightarrow T$
具体的には、Haskellの場合は以下のように表現することができます。
- 自己関手 $T : $
- 対象関数 $:$
m // 型コンストラクタ - 射関数 $:$
fmap :: (a -> b) -> (f a -> f b)
- 対象関数 $:$
- 自然変換 $\eta : \mathrm{Id}_\mathbf{C} \Rightarrow T$
- 射関数 $:$
pure :: a -> m a
- 射関数 $:$
- 自然変換 $\mu : \mathrm{T^2} \Rightarrow T$
- 射関数 $:$
join :: m (m a) -> m a
- 射関数 $:$
※ joinの実装はbindを用いて定義可能、join x = x >>= id
またモナドは上記を満たしたうえで、前述したモナド則と言われる3つの性質を満たす必要があります。
これらは以下のようにbindとpureを使って表現することができます。
- 左単位律:
pure a >>= f == f a - 右単位律:
m >>= pure == m - 結合律:
(m >>= g) >>= h == m >>= (\x -> g x >>= h)
以上より、本記事の文脈では以上のようにbindとpureの操作を持ち、モナド則を満たすものをモナドと定義します。
モナドにできること
値に文脈・計算効果を付与する
モナドを使うと文脈や計算効果を伴う値・計算を表すことができます。
たとえば、Maybeといった型を使うことで、暗黙的だった失敗可能性を型として表現できます
これにより、戻り値がMaybeであることが型シグネチャに現れるため、呼び出し側は失敗可能性を認識できます。また、通常の値として安全に取り出す際にはNothingの場合を考慮する必要があります。
例えば以下の関数を呼び出した時、引数によって値を取得できない可能性があります。
findUser :: Int -> Maybe String
findUser 1 = Just "Alice"
findUser 2 = Just "Bob"
findUser _ = Nothing
その場合、Maybeが戻り値であるとシグネチャに表現されていれば呼び出し側は失敗する可能性を考慮する必要があると分かり、失敗ケースの見落としを防ぎつつプログラムを書くことができます。
case findUser 1 of
Nothing -> ...
Just u -> ...
このように値に文脈を付与することにより、暗黙的であった失敗可能性という意図をプログラム上で表現することができるようになりました。
他にもIOモナドや、Eitherモナド、Stateモナド、Listモナドなどが有名どころとしてあり、どれも文脈・計算効果を付与するという点で共通しています。
統一的なインターフェースで操作する
本記事で扱うプログラム上のモナドは、bindとpureを持っていることが保証されています。
そのため、モナドに対しては上記の動作を持っていることを前提にした特別な構文を用意することが可能です。
具体的にはHaskellで言うところのdo記法です。
この記法ではモナド関数を繋ぐ際のbindチェーンを直感的に書くことができます。
findUser :: Int -> Maybe User
findCompany :: User -> Maybe Company
findAddress :: Company -> Maybe Address
do
user <- findUser 1
company <- findCompany user
address <- findAddress company
return address -- pure addressと同じ意味
<-は、各関数が返したモナドの中の値を束縛しており、上記の記述は以下の実装と同じように評価されます。
findUser 1 >>= \user ->
findCompany user >>= \company ->
findAddress company >>= \address ->
return address
do記法を使えば上記のように、前提となる情報が取得できたら次の処理を実行するというモナドの関数合成の流れを手続き的に記述することが可能です。
Scalaでもfor内包表記というHaskellに非常に似た構文があります。
for {
user <- findUser(1)
company <- findCompany(user)
address <- findAddress(company)
} yield address
またClojureにもdomonadというマクロがあり、これを利用することで同様の書き心地で利用できそうです。
(domonad maybe-m
[user (find-user 1)
company (find-company user)
address (find-address company)]
address)
まとめ
今回は複数言語のモナドを見ながらモナドの基本的なことを学びました。
プログラミングの文脈では上記の事が分かっていればモナドを利用していくことができると思います。
モナドの使い所は、失敗・状態・IOなどの文脈や計算効果を持つ計算を、その文脈を保ったまま関数合成したい場面です。特に、前の計算結果を使って次の計算を決めたい場合に力を発揮します。
失敗可能性や副作用などといった文脈を付与したい場面があれば、ぜひ積極的に使っていきましょう。