絵で見るモナド
John Wiegley さんの “Monads in Pictures” を翻訳しました。翻訳の公開は本人より許諾済みです。翻訳の間違い等があれば遠慮なくご指摘ください。
2012年8月20日 John Wiegley 著 2012年8月21日 e.e d3si9n 訳
これはモナドのチュートリアルではないし、ここには数学用語も出てこない。本稿は、既にモナドを一応使えるぐらいには習った人を対象とする。視覚化することで、何のために何をやっているかが明らかになるはずだ。
関数
モナドに対する直感を得る一つの方法として関数からモナドへの抽象化をたどるというものがある。関数が何をやっているのかを簡単な絵で表してみよう。Haskell の関数の呼び出しの構文を上に、同じ演算を視覚化したものを下に置いた:
関数はある値 a
を投射 (map) して別の値 b
を得る。中で何が起こっているかは誰も知らないが、普通は何らかの計算が行われる。僕の個人的なプログラミングの経験から全ての関数はなんらかの処理を実行しなくてはいけないと思っていたこともあったが、それは関数を実装する方法の一つにすぎない。抽象的には、関数は値が別の値になる通り道だ。
Functor
抽象化の次の段階は Functor だ。何故 Functor が必要かって? a
から b
に行く事だけが全てじゃないからだ。a
が別の値をラッピングする (または含むか提供する) と知っていて、本当にやりたいのは「a
の中身の値」に関数を適用することだからだ。
リストがこれにあたる。整数のリストがあるとき、関数をリスト自身じゃなくてリストの中の整数に適用したいときがある。だから、リストは Functor となる最適な候補だ (実際に Functor だ)。
実際に欲しいものへの「コンテキスト」を表すために以下の図ではブラケットを用いた。しかし、これらのブラケットはリストではなく、「コンテキスト」を表す:
ここでは前と同じ関数を使っている。違いは a
から b
に直接投射する代わりに関数 fmap
は:
- 渡された Functor を「アンボックス」して
- その値に
f
を呼び出して新しい値に変換して - 結果を同じ形と種類だけど別の Functor に「ボックス」する
注意: ここではボックスや形などの物理的なメタファーを用いたが、Functor が常に物理的なものだという誤解をしないようにしてほしい。Functor そのものが関数であることも可能で、その場合は「コンテキスト」はコンテナではなく計算をモデル化する。Functor をどう考えれば一番いいのかはそれが何を実装しているのかに依存する。
モナド
信じられないかもしれないけど、モナド (Monad) は Functor をちょっといじっただけのものだ。インターネットだけを見ていると数学者だけが理解する何か高度に特殊化した何かだと思うけもしれない。しかし、真実は Functor を理解できればモナドとその栄華の全てを理解する一歩手前だということだ。
上で fmap
は 3つのことをすると言った: Functor をアンボックスして、箱に入っていた値に関数を適用して、結果を同じ形と種類の新しい Functor にボックスする。これが Functor の魂だ。
Monad はほぼ同じ事を実行する! ただ一つの違いがあって、それは結果の値をボックスしないことだ。結果のボックス化は必要なんだけども、その分担はモナドから君の関数へと移行する。
モナドの振る舞いを絵で見てみよう:
えっ?! これは Functor と同じ絵じゃないか! よく注意してみて欲しい。適用を表すグレーの矢印は a
から b
へ行くかわりに、a
から b
のコンテキストへ行っているはずだ。また、fmap f [a]
と呼び出すかわりに、引数をスワップする中置関数 [a] >>= f
が使われている。
これがモナドの違いの全てだ。この絵で見えないのは何故モナドが素晴らしくて、変更点が何を意味するのかということだ。
僕たちの関数が新しいコンテキストを返すため、このコンテキストを変えることができるようになった。これは Functor には真似できない。Functor に関数を投射すると、結果は常に同じ形と種類の新しい Functor だ。しかし、モナドに関数を bind すると (用語の違いに注意) 結果は同じ種類だけど別の形の新しいモナドであることができる。この別のものを返せる能力がモナドの強みだ。
以下のモナディックな bind の連鎖を見てよう:
この連鎖のそれぞれのステップにおいてコンテキストが変わることがきる。可変状態、トーケンストリーム、補助結果の値、エラーコードなど様々なものを持つことができる。参加する関数が中間値を新しいモナドにボックス化するという役割を担って、bind 演算に直接関わることでこれらが全て可能となる。
この新しい責任は重荷ともなりえる。モナドのことを何もしらない純粋関数をモナドに bind できなくなったからだ。最低でも関数に liftM
を呼び出してモナドを知っている新しい関数を得る必要がある。時にはこのリフティングをしたり「モナディックなコンテキスト」を意識しなきゃならないことがダルくなってくることもある。
しかし、これが全てだ: 関数は値同士を関連付ける能力を与え、Functor はコンテキスト内の値を関連付ける能力を与え、モナドは bind 演算の連鎖を用いてコンテキストを運ぶ能力を与える。
Arrow
Monad という抽象体がコンテキストを保持するのが関数の間でやり取りされる値であることが用途に合わないことがある。コンテキストがデータじゃなくて演算を囲ってほしいこともある。Arrow が提供するのがそれだ。簡単に言うと、Arrow は値の投射という概念そのもの (つまり、関数が提供するサービスのことだけど、他にも値を投射する方法はある) にコンテキストを与える。事実、どの関数でも arr
関数を使って Arrow に変換できる。
これは上の関数呼び出しの例を Arrow 演算にアップグレードしたものだ:
run<Arrow>
の使い方に注意してほしい。それぞれの Arrow は独自の実行方法を提供する (実行機能を公開しないこともある)。例えばライブラリが完全に不透明な Arrow を提供して制御された条件下でのみ実行することが考えられる。そのため、ユーザが知るべきは Arrow へのインプットとアウトプットだけだ。そのようなタイプの Arrow が合成されると呼び出す関数など、さまざまな種類の情報が他にあるかもしれない。
それでは Arrow は何に使えるだろう? 関数をコンテキストに包んで渡して欲しいときに使える。例えば、データベースを問い合わせる関数を別の関数に渡したいとする。通常は (遅延評価のお陰で) クエリを呼び出して戻り値を渡せば、実際に戻り値が必要な時にクエリが実行される。しかし、関数がクエリを何回も実行する必要があったらどうだろう? その場合は呼び出し元がクエリを実行する必要がある。
普通の関数だとクエリ関数とクエリを実行するデータベースへのハンドルの両方を渡す必要がある。もしくは、リーダーモナドを使うが、その場合はクエリを実行するコードにモナドの存在を感染させる必要がある。データベースのハンドルをクエリとひとまとめにして、自身がどのデータベースと話すかを知っている強化されたクエリ関数が作れれば一番良い。Arrow 登場だ。
もっと興味深い Arrow の用法は強化された合成だ。コンテキスト付きの Arrow にもう一つのコンテキスト付きの Arrow を様々な方法で合成して合成されたコンテキスト付きの合成 Arrow を作ることができる。その合成が何を意味するかは関わっている Arrow の型による。
Applicative Functor
ボーナスで Applicative Functor も見てみよう (多分モナドをよりよく理解する役には立たないと思うけど)。
Applicative は Functor をある方法でアップグレードする。fmap
が一つの値から別の値にいく関数しか受け付けないのに対して Applicative は任意の数の引数を受け取る関数を同数の Applicative Functor へと投射できる:
a
を提供する Functor に対して a
から b
へ行く関数 f
を投射するのにかわって、この例では 4つの引数を取る f
を 4つの別の Functor へと一度に適用できる。
これは Appticative を完全に説明するものではないけど、これが肝となる。Control.Applicative
モジュールは普通の関数できるようなカリー化、合成、シーケンス化など Applicative の適用に利用できるヘルパー関数群を提供する。直感の鍵となるのは引数が何個あっても関数でも Functor の世界で使える関数に変換できるということだ。これができれば、君の関数は Applicative のコンテキスト内でコンテキストに関することを一切知らないまま自由に使いまわすことができる。