日記 (2019 年 3 月 8 日)
Haskell のモナドについてちょっと分かったので、 備忘録としてまとめておきます。
例えば、 Int
型の値 x
と Int -> Int
型の関数 f
があったとしたら、 関数適用によって Int
型の値である f x
が得られます。
しかし、 現実的にはこんな綺麗な関数ばかりではなく、 計算の途中で失敗してしまう可能性があって値を返せないかもしれない関数や、 計算した結果返すべき値が複数になるかもしれない関数などがあります。
これらのある種 「綺麗でない」 関数は、 計算の結果以外に 「文脈」 と言われる付加的な情報を変化させていると考えられます。
計算が失敗するかもしれない関数であれば、 文脈というのは成功したのか失敗したのかという情報です。
計算結果が複数個になるかもしれない関数であれば、 文脈は計算結果を 1 つに決定できなかったという情報です。
また、 計算途中のログをコンソールに出力したいというのはよくあることですが、 このような動作をする関数ならば、 文脈はその出力と考えられます。
これらの文脈をもつ関数を抽象化して統一的に扱えるようにしたのが、 Monad
クラスです。
Monad
クラスを扱う際に非常に重要になるのが、 return
関数と >>=
演算子です。
以下のように定義されています。
なお、 実際のコードでは Applicative
クラスを拡張する形で定義されていますが、 ここでは無視します。
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
-- その他の定義
m
がモナドであるとき、 m a
型の値とは m
が定める何らかの文脈をもった a
型の値です。
文脈が扱えるように a
型の値を包んだものと見なしておくと分かりやすいでしょう。
最初の >>=
は、 関数適用のようなものです。
文脈をもつ m a
型の値 x
と、 a
型の値から文脈が発生し得る計算をして m b
型の値を返す関数 f
があったしましょう。
x
は文脈をもっているので、 直接 f
の引数として渡すことはできません (当然型も合っていない) が、 x >>= f
と書くことで、 文脈を良い感じに処理して f
に渡すことができるようになります。
この 「文脈を良い感じに処理して渡す」 というのが >>=
の役目です。
次の return
は、 普通の値にデフォルトの文脈をもたせるような関数です。
普通の値として扱いたいけれども m a
型の値にしたいときに、 return
を使って何もない文脈をもたせるような感じです。
さて、 具体例を見てみましょう。
まずは、 計算に失敗するかもしれない関数を扱いたい場合、 つまり文脈として成功か失敗かを扱いたい場合を考えましょう。
これを実現するのが、 Maybe
モナドです。
Maybe
モナドは、 上で紹介した 2 つの関数を以下のように実装しています。
実際のソースコードとは違うのですが、 実質同じです。
class Monad Maybe where
-- 関数適用っぽいやつ
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
Just x >>= f = f x
Nothing >>= _ = Nothing
-- 文脈をもたせるやつ
return :: a -> Maybe a
return x = Just x
まずは >>=
の方から見ていきましょう。
1 つ目の引数は Maybe a
型の値 x'
です。
つまり、 これまでの計算に成功して何らかの値を持っているかもしれないし、 失敗してしまって何もないかもしれない、 そんな状態を表す値です。
2 つ目の引数は a -> Maybe b
型をもつ関数 f
です。
これは要するに、 a
型の値をもらって b
型の値を計算するわけですが、 途中で失敗して値を返せないかもしれない関数です。
この f
に x'
を良い感じに適用させたいわけですが、 どうなるのが嬉しいでしょうか。
もし x'
の計算に成功していたら、 それは何らかの値をもっているわけなので、 そのまま f
に適用した結果が得られるのが自然でしょう。
そうではなく x'
の計算に失敗していたら、 もうすでに失敗しているわけなので、 f
の適用に関しても失敗したと考えて、 全体として失敗したことになるのが自然です。
そのことを踏まえて実装を見ると、 まさにその通りになっているわけです。
次に return
の方ですが、 これは a
型の値 x
が与えられて、 その値から成功か失敗かの文脈をもたせた値を返してほしい関数です。
すでに x
という明確な値があるわけなので、 これは成功したという文脈をもたせるのが自然でしょう。
Maybe
には Just
というまさにそれを表すコンストラクタがあるので、 return
は Just
そのものだと定義されています。
別の例として、 結果が 1 つに定まらず複数になってしまうような計算を扱いたい場合を考えてみましょう。 これを表すのがリストモナドです。
リストモナドというのはただのリストのことですが、 モナドであることを強調するときに 「リストモナド」 と呼ぶことがあるそうです。
a
型のリストは [a]
で表すことが多いですが、 これは [] a
とも書けて、 この型コンストラクタ []
がモナドになっています。
実装は以下です。
class Monad [] where
-- 関数適用っぽいやつ
(>>=) :: [a] -> (a -> [b]) -> [b]
xs >>= f = [y | x <- xs, y <- f x]
-- 文脈をもたせるやつ
return :: a -> [a]
return x = [x]
関数適用っぽい方の >>=
を見てみましょう。
1 つ目の引数は [a]
型の値 xs
で、 これまでの計算の結果が 1 つに定められずにリストに収められていると見なしてください。
2 つ目の引数は a -> [b]
型をもつ関数 f
で、 これは a
型の値を使って計算をするもののその結果が 1 つに定められずリストを返す関数です。
このようにリストを 1 つに定めきれなかった結果の集まりと見なしたとき、 f
に x
を良い感じに適用させたらどうなるべきでしょうか。
あり得る値 (リストに入っている値) 全てに対して f
の結果を計算して、 それらの計算結果を全て集めたものとするのが自然でしょう。
定義を見ると、 [y | x <- xs, y <- f x]
となっているわけですが、 まさに今言った処理をしています。
return
の方はというと、 a
型の値 x
が与えられるわけですが、 これは計算の結果が 1 つに定まったものだと見なせるので、 x
だけからなる単項リストを返すのが自然です。
実際、 そのように定義されていますね。
ということで、 モナドというものは、 文脈付きの値や関数を、 その文脈が具体的に何であったとしても、 統一的に良い感じに扱える枠組みを提供してくれるわけです。
モナドに関してはまだまだいろいろな機能があります。
モナドの代表例であるところの IO
についてまだ触れてませんし (というかまだ私が分かってない)、 モナドの計算を直感的に書ける do 記法というものもあります (これもまだ私が分かってない)。
この辺りは別の機会で触れようと思います。