ハスケルプログラマーにとっての通過儀礼は、モナドの仕組みをようやく理解したと思ったときに「モナドチュートリアル」のブログ投稿を書くことだという一般的なジョークがあります。しかし、そういった投稿は十分に存在するので、これがまた別のモナドチュートリアルになるつもりはありません。しかし、私の学習経験に基づいて、なぜ人々がモナドに苦しむのか、そしてその結果、なぜそんなに多くのチュートリアルが存在するのかについての考えがあります。
高いレベルで見ると、モナドの直感は、プログラミングにおけるシーケンシングの抽象化であるということです。「これを行い、その後前の結果を使ってあれを行う」という計算は、モナディックと見なすことができます。
Monad
を実装するいくつかの型、私が「計算モナド」と考えるIO
やState
のようなものにとって、この直感は意味があります。これらの型は「入力 / 出力操作を実行する」や「状態を追跡しながら値を計算する」といった動詞を表すため、計算として考えがちです。この意味で、これらのモナドインスタンスは、これらの計算がどのように順次組み合わされるかを説明していると解釈します。
多くのハスケルの新参者がモナドの仕組みを理解するのに苦労するのは、私が「データモナド」と呼ぶ、見かけ上無関係な型のグループにこの直感を適用しようとする時です。これらはリストやMaybe
のようなもので、通常は「値のリスト」や「オプションの値」として名詞を表すと考えます。問題はこれです:これらのデータ型をどのようにシーケンスすることができるのか? それらはどのようにモナドなのでしょうか?
根本的に、この問題はモナドがデータと計算の境界をぼやけさせるという事実から生じていると思います。通常、これら二つは異なる概念だと考えています。データモナドを理解するためのコツは、それらを名詞としてではなく、動詞として計算として解釈することだと主張します。
しかし、このデータと計算の間のぼやけた境界は、モナドに特有のものではありません。実際、関数型プログラミングの核心には、まさにそのぼやけた境界が存在します:第一級関数です。
計算をデータとして#
第一級関数、つまり関数を通常の値として表現する能力は、関数型プログラミングのパラダイムの核心的な側面です。第一級関数を持つ言語では、任意の関数が別の関数を返したり、関数を引数として受け取ったりすることができます。
また、データと計算の間のぼやけを正確に表現します。通常、関数は計算として考えます:何かを入力として受け取り、出力を計算するものです。しかし、第一級関数を使うことで、関数をデータとして使用することができます — 文字列や整数のように渡されるオブジェクトとして。
しかし、関数だけが計算のタイプではありません。少なくとも、私たちはしばしばIO
やState
のようなより複雑な計算の種類で作業します。これらは、関数の通常の入力と出力の外で他の効果を追跡しながら値を計算する型です。
ハスケルの型システムが非常に強力である理由の一部は、これらの他の計算の型をデータとしてエンコードする能力です。実際、State
の基本的な定義を見ると、特定の関数シグネチャの上にある一般的なラッパーに過ぎないことがわかります:
newtype State s a = State (s -> (a, s))
つまり、型State s a
は、初期状態を入力として受け取り、更新された値と状態を返す関数です。この関数をデータ型でラップすることで、状態を持つ計算をデータとしてより簡単に扱うことができます。つまり、関数への入力または出力として使用したり、他の計算にラップしたりすることができ、これがより実用的なStateT
モナドトランスフォーマーが行うことです。
一般的に、計算をデータ型として扱う技術はハスケルコードに広く浸透しています。これにより、State
やIO
のような副作用のあるコードを純粋なコードからスコープし、隔離し、複数の効果のある計算を組み合わせることができます。
では、モナドとは何でしょうか?すべてのモナドは、順次組み合わせることができ、いくつかの副作用を伴う計算、つまり動詞やアクションを表します。しかし、これが時には不明瞭です。なぜなら、私たちはしばしばこれらのアクションを普通のデータ型のように扱うからです。
データを計算として#
では、リストやMaybe
のような「データモナド」はどうでしょうか?これらも計算として解釈することができ、そのMonad
インスタンスは、私たちがそれらをそのように扱うための豊富な一般的ツールセットにアクセスできるようにします。
このアイデアをよりよく説明するために、例を見てみましょう。Maybe
モナドを使ってデータを計算として表現することを考えます。前述のように、私たちは通常Maybe a
を、そのデータの存在(Just a
)またはそのデータの不在(Nothing
)を表す一般的なデータ型として考えます。
しかし、Maybe
のモナドインスタンスは、同じ型の異なる解釈を利用しています。代わりに、Maybe a
は副作用のある計算として見ることができます。つまり、a
を計算する過程で、計算が何も返さない可能性があるということです。もしそうなれば、Nothing
を返し、そうでなければJust a
を返します。
Maybe
を計算として考えると、その型のモナドインスタンスがより理解しやすくなるかもしれません。モナディックバインド演算子(>>=
)の実装は、内部の値をスレッドしながら二つの計算を順次実行する方法を教えてくれます。Maybe
のモナドを次のように実装できます:
instance Monad Maybe where
m1 >>= m2 = case m1 of
Just x -> m2 x
Nothing -> Nothing
このインスタンスは、二つのMaybe
計算、m1
とm2
をどのように連結するかを教えてくれます:最初の計算が値を返す場合(Just x
)、シーケンスはその値をラップした次の計算の結果を返します(m2 x
)。そうでなければ、最初の計算がNothing
を返す場合、シーケンスもNothing
を返します。
本質的に、この実装は、Maybe
計算のシーケンスが、途中の計算がNothing
を返さない限り、内部の値をチェーンに渡すことを意味します。この場合、全体のシーケンスはNothing
を計算します。したがって、Maybe
のようなデータがどのようにシーケンスされるのかを理解するのが難しいかもしれませんが、Maybe
を計算として考えると、それをシーケンスする方法の疑問がより直感的になります。
これはもちろん一つの例に過ぎず、Maybe
のバインドの実装は特にシンプルなものですが、これは重要なポイントを示しています:型のモナドインスタンスを理解しようとする際には、まずその型がどのような計算を表しているのかを理解し、その後、二つの計算がどのようにシーケンスされるかに取り組むべきです。
リストモナドにも同様のデータ - 計算の平行が存在し、リストを非決定的な計算、すなわちすべての可能な実行経路を探索し、それぞれの値を統合するものとして考えます。このリストの平行な解釈は、モナドインスタンスを実装する方法を理解するのに役立つかもしれません。
結論#
要約すると、ハスケルはデータと計算の相互作用を許可し、奨励します。その型システムはすべての計算を第一級の値として位置づけ、データとしての計算の豊かな表現を可能にします。一方、モナドは、シーケンスされ、連結される可能性のある計算としてデータ型を扱うための一般的なツールセットを提供します。
これら二つの概念を把握することは、ハスケルのモナドを扱う方法を理解する上で重要でしたので、これがあなたにも役立つかもしれません。