モナドはメタファーではない
Scala界の関数型プログラミング一派を代表する論客の一人、@djspiewak が 2010年に書いた “Monads Are Not Metaphors” を翻訳しました。翻訳の公開は本人より許諾済みです。翻訳の間違い等があれば遠慮なくご指摘ください。
2010年12月27日 Daniel Spiewak 著 2011年5月29日 e.e d3si9n 訳
僕は今、約束を破るところだ。およそ三年前、僕は絶対にモナドの記事だけは書かないと自分に約束した。既にモナドに関する記事は有り余っている。記事の数が多すぎてその多さだけで多くの人は混乱している。しかも全員がモナドに対して異なる扱い方をしているため、モナドの概念を初めて学ぼうとする者は、ブリトー、宇宙服、象、砂漠のベドウィン (訳注: アラブ系遊牧民) の共通項を探す努力をするハメになっている。
僕は、この混乱した喩え話のサーカスにわざわざもう一つ追加するようなことはしない。まず、どの喩え話も完全には正確では無い。どの喩えも全体像を伝えきれていないし、いくつかは重要な点に関して露骨に誤解を招くような内容になっている。メキシコ料理や宇宙(そら)に思いをはせることでは、絶対にモナドを理解することはできない。モナドを理解する唯一の見方は、それをありのままの姿、つまり数学的概念として見ることだ。
数学(もしくは、それ以外の何か)
モナドを分かりづらくしている事に、モナドはパターンであり、特定の型ではないことが挙げられる。モナドは形であり、また具体的なデータ構造である以上に抽象的なインターフェイス(Java での interface という意味ではない)なのだ。結果として、喩えに基づいたチュートリアルは不完全性と失敗の運命にある。本当に理解するには、一歩下がって、具象ではなく、モナドが抽象的に何を意味するのかを見ていく必要がある。次の Ruby のコード例を見て欲しい:
def foo(bar) puts bar bar.size end
ここで Ruby の復習をすると、このコードは以下のように書き換えることができる:
def foo(bar) puts bar; bar.size end
Ruby には便利な(最近ではほとんどの言語が採用している)ルールによりメソッドの最後の式が、暗黙の return文となる。そのため、foo
メソッドは、一つのパラメータを取り、それを標準出力に表示し、その size
を返す。簡単だよね?
ここでクイズ。セミコロン (;
) は何をやっているのだろう?ただの分離体だと言ってしまうのは簡単だが、理論的には、もっと興味深いことが起こっている。ここで Scala に切り替えて、さらにクリスマスのオーナメントも付け足してみよう:
def foo(bar: String) = {
({ () => println(bar) })()
({ () => bar.length })()
}
Scala に詳しくない人のために誤解を生まないよう言っておくと、全ての文をラムダ式(匿名関数)で囲う必要は全く無い。説明のために敢えてこうしているだけだ。
この関数は Ruby のバージョンと全く同じ事をする。まあ、パラメータに、size
を定義する全ての値の代わりに String
を要求する分は制限されていると言えるが、気にしない事にする… 前にあったコードとの大きな違いは、それぞれの文が直後に呼び出される匿名関数に囲まれていることだ。Ruby 同様にセミコロンを使うこともできるが、これらの文が実際には関数であるため、もう一段階ひねることができる:
def foo(bar: String) = {
({ () => println(bar) } andThen { () => bar.length })()
}
(注意: 実際には andThen
メソッドは 0-arity の関数には定義されていないが、ここでは定義されており、一つの引数を取る関数と同じ振る舞いをするふりをする。もしそう考えた方が落ち着くなら、両者とも Unit
をパラメータと取る 1-引数の関数だと考えることができる。表記は増えるが、理論的には同じ結果となる。)
(使うこともできたが、)ここではセミコロンを使わなかったことに注目して欲しい。その代わりに、二つの関数を組み合わせて、それを最後に呼び出した。この組み合わせの意味論を追っていくと、まず第一の関数が評価され、その戻り値 (()
) が捨てられた後、第二の関数が評価され、その戻り値が返されている。ご家庭でご覧の皆様のために解説すると、andThen
は以下のように定義することができる:
def funcSyntax[A](f1: () => A) = new {
def andThen[B](f2: () => B) = f1(); f2()
}
見方によっては、関数に直接適用するか文のレベルで間接的はたらくかの違いこそあれ、セミコロン「演算子」の能力を文字通り内包するメソッドを定義したと考えることができる。それはそれで面白い考えだが、重要なのは、まず最初の関数を実行し、その結果を捨てたあと、第二の関数を実行して、その結果を返しているということだ。
これが任意の数の関数に適用できることは明らかだろう。例えば:
def foo(bar: String) = {
({ () => println("Executing foo") } andThen
{ () => println(bar) } andThen
{ () => bar.length })()
}
ついてきてるかな?おめでとう。これが初めてのモナドだ。
君がモナドを発明できていたかもしれない!
これは、従来の意味でのモナドではないかもしれないが、少し頑張ればこれがモナド則を満たすことを証明できる。重要な点はこのモナドが何をしているかという点だ: 何かを順序に従って組み合わせている。事実、突き詰めれば、全てのモナドがしていることも同じ事だ。まず、物体一号から始め、次に(一号を与えると)物体二号を返す関数がある。モナドは物体一号と関数を組み合わせて最終的な物体を導くことができる。もう少しコードを見てみよう:
case class Thing[+A](value: A)
これは恐らく想像できる限り最も単純なコンテナだろう(実は、これはまさに想像できる限り最も単純なコンテナなのだが、その点は今は重要ではない)。値を Thing
で囲う以外は何もできない:
val a = Thing(1)
val b = Thing(2)
ここで頭を設計モードに切り替えて欲しい。以下のようなコードを頻繁に書かなくてはいけないと想像して欲しい:
def foo(i: Int) = Thing(i + 1)
val a = Thing(1)
val b = foo(a.value) // => Thing(2)
Thing
から始めて、その Thing
内の値を使って関数を呼び出して、それが新たな Thing
が得られる。よく考えると、これはよくあるパターンであることに気づく。まず値があり、その値を使って新しい値を計算する。数学的は、これは以下とほぼ同じだ:
def foo(i: Int) = i + 1
val a = 1
val b = foo(a) // => 2
唯一の違いは、最初のバージョンが全てを Thing
で囲っているのに対して、第二のバージョンは「生」の値を使っているということだけだ。
ここで想像力を少し使って全てを Thing
で囲む利点があると仮定しよう。もちろん、これには数々の理由があるかもしれないが、Thing
にその値に何か面白いことができるロジックが何かあるという考えにまとめる事ができる。問題はこれだ: a
から b
に行くのにより良い方法を考えられるだろうか?基本的には、このパターンをより一般化したツールとしてカプセル化したい。
僕らが欲しいのは、Thing
から値を取り出し、その値を用いて別の関数を呼び出し、呼び出しのその戻り値(新たな Thing
)を返すという関数だ。僕らは善良なオブジェクト指向プログラマであるため、これは Thing
クラスのメソッドとして定義される:
case class Thing[+A](value: A) {
def bind[B](f: A => Thing[B]) = f(value)
}
よって、ある Thing
があれば、その値を取り出し新たな Thing
を計算する、というステップを一発で実行できる:
def foo(i: Int) = Thing(i + 1)
val a = Thing(1)
val b = a bind foo // => Thing(2)
これは、元のバージョンと全く同じ機能があるが、よりスッキリしていることに注目してほしい。Thing
はモナドだ。
モナドパターン
もし何かから始めて、それを分解した後で、同じ型の別の何かを計算した時、それはモナドだ。単純だが、本当にそれだけだ。もしほとんどのコードはそれで説明できるじゃないかと言うなら、その通り。だんだん分かってきた証拠だ。モナドはいたる所に存在する。「いたる所」は本当にいたる所という意味だ。
これを理解するためには、何が Thing
をモナドたらしめているのかを見てみよう:
val a = Thing(1)
第一に、任意の値を新たな Thing
で囲むことができる。オブジェクト指向のプログラマはこれを「コンストラクタ」と呼ぶかもしれない。モナドでは、これは unit
関数と呼ばれる。Haskell はこれを return
と呼ぶ(ちょっとこれは後回しにしたほうがいいかな)。とにかく、なんと呼ぼうとやっていることは同じだ。型が A => Thing
の関数があって、それは何らかの値を取り、新たな Thing
でラッピングする。
a bind { i => Thing(i + 1) }
次に bind
関数がある。これは Thing
から値を掘り出して、渡された関数を使って新たな Thing
を作り出す。Scala はこれを flatMap
と呼ぶ。Haskell は >>=
と呼ぶ。繰り返すが、名前は関係無い。大切なのは、bind
が二つの物を順序に従って組み合わせていることだ。ある物から始めて、その値を使って新たな物を計算するのだ。
こんなに単純なことだ!僕と同じような考えなら、以下の疑問を持っていることだろう: もしそんなに単純なら、なんで皆大騒ぎしてるの?何で単に「一つの物を使って別の物を計算する」パターンって呼べばいいんじゃないの?
取り敢えず言っておくと、その名前は長過ぎるのでダメ。次に、モナドは最初に数学者によって定義された。数学者は何にでも名前をつけるのが好きなのだ。数学はパターン探しの専門だが、探した後で何でもいいから名前を付けないと、そのパターンを後で探すのが難しくなるからだ。
他の例
モナドはいたる所にあると言ったが(本当だよ!)、まだ二つしか例を見ていない。他にもいくつか見てみよう。
Option
これは恐らく最も有名なモナドで、また最も簡単に理解でき、その動機も分かりやすいモナドだ。以下に具体例で説明する:
def firstName(id: Int): String = ... // データベースから取得
def lastName(id: Int): String = ...
def fullName(id: Int): String = {
val fname = firstName(id)
if (fname != null) {
val lname = lastName(id)
if (lname != null)
fname + " " + lname
else
null
} else {
null
}
}
またしても、ありがちなパターンだ。ここに二つの関数 (firstName
と lastName
) があり、それぞれ利用可能か不可能か分からないデータを取得する。もしデータがあれば、その値が返る。それ以外の場合は、null
を返す。次に、これらの関数を使って何か面白いことをする(この場合、フルネームを計算する)。残念ながら firstName
と lastName
が役に立つ値を返すか返さないのかは明示的に入れ子になった if
によって処理される必要がある。
パッと見ではこれ以上何もできないかのように見える。しかし、注意深く見るとこのコードにモナドパターンが隠されていることが見える。前回よりも少し複雑だが、そこにあることはある。まず、全てを Thing
で囲んでみよう:
def firstName(id: Int): Thing[String] = ... // データベースから取得
def lastName(id: Int): Thing[String] = ...
def fullName(id: Int): Thing[String] = {
firstName(id) bind { fname =>
if (fname != null) {
lastName(id) bind { lname =>
if (lname != null)
Thing(fname + " " + lname)
else
Thing(null)
}
} else {
Thing(null)
}
}
}
見えたかな?繰り返すが、モナドはいたる所にある。
ここで気付いたことだけど、bind
を呼ぶたびに、関数の中で最初にしていることは、毎回、値が null
かチェックしているということだ。それならそのロジックを bind
に移せばいいんじゃないか?もちろん、Thing
をいじらないことにはそれは実現できないから、新しいモナド Option
を定義しよう:
sealed trait Option[+A] {
def bind[B](f: A => Option[B]): Option[B]
}
case class Some[+A](value: A) extends Option[A] {
def bind[B](f: A => Option[B]) = f(value)
}
case object None extends Option[Nothing] {
def bind[B](f: Nothing => Option[B]) = None
}
Some
以外のものを無視すると、これは慣れ親しんだ Thing
と似通っている。主な違いは、Option
に二種類のインスタンスがあることだ: 値を含む Some
と値を含まない None
だ。None
は Thing(null)
と簡単に書く方法だと考えればいい。
面白いのは、Some
と None
で二つの異なる bind
の定義が必要なことだ。Some
の中の bind
の定義は Thing
のものにそっくりだ。これは、Some
と Thing
がほぼ同一なことで説明がつく。しかし、None
は渡された関数を無視して常に None
を返す bind
を定義する。これがどうして役立つかって?fullName
の例に戻そう:
def firstName(id: Int): Option[String] = ... // データベースから取得
def lastName(id: Int): Option[String] = ...
def fullName(id: Int): Option[String] = {
firstName(id) bind { fname =>
lastName(id) bind { lname =>
Some(fname + " " + lname)
}
}
}
これで全ての不愉快な if
文が無くなった。これは firstName
と lastName
の両者ともがデータベースのレコードの取得に失敗すると Thing(null)
の代わりに None
を返すために機能する。もちろん、None
に対して bind
しようとしても、戻り値は常に None
だ。よって、fullName
関数は、firstName
と lastName
の両者共のが None
では無い時に値の組み合わせを Some
に入れて返す。
ご家庭で得点をつけている皆様、僕らは「偶然」にも Groovy の安全な参照演算子 (?.
)、Raganwald の Ruby のための andand
など、などを発見しました。どうだろう?モナドはいたる所にある。
IO
モナドを理解しようとすると、いずれぶつかる壁に Haskell の IO
モナドがあり、結果はいつも同じだ: 狼狽、混乱、激怒、そして最終的には Perl。実際の所は、IO
はモナドの中では変わり者なのだ。基本的には Thing
や Option
と同じカテゴリーにあるのだが、全く異なる問題を解決する。
説明しよう。Haskell は副作用を許さない。一切許さない。関数はパラメータを取り、値を返す。そのため、関数の外の何かを勝手に「変更」することはできない。以下に以前の Ruby のコードを具体例として、これが何を意味するのかを説明する:
def foo(bar) puts bar bar.size end
この関数は値を取り、size
メソッドを呼び出し、その結果を返す。しかし、同時に標準出力ストリームを変更する。これは、グローバルな配列を in-place で変更するのとほぼ変りない。
STDOUT = []
def foo(bar) STDOUT += [bar] bar.size end
Haskell には変数が一切無い(Scala に var
が無いか、Java の全ての変数が final
であることを想像してみればいい)。変数が無いため、in-place で何も変更することはできない。in-place で何も変更することできないため、puts
関数を実装するのは無理だ。少なくとも、僕らが知っている形での puts
は無理だ。
Scala に戻ろう。目標は、可変状態に一切依存せずに println
関数を定義することだ(ただし、物理的なディスプレイをクローニングしてユーザの画面を「変更」することは不可能なため、一時的に標準出力ストリームは常に可変であることを無視する)。標準出力ストリームを Vector
としてラッピングして、これを関数に渡していくことでこの問題は回避できる:
def foo(bar: String, stdout: Vector[String]) = {
val stdout2 = println(bar, stdout)
(bar.length, stdout2)
}
def println(str: String, stdout: Vector[String]) = stdout + str
このように現在の stdout
を関数に渡し、新しい状態を戻り値として返すことで、理論的には全ての println
を用いた関数を書くことができる。最終的にプログラムは結果と共に stdout
の最終状態を返し、言語ランタイムがそれを画面に表示すればいい。
これは(一応 println
に関しては)うまくいくが、途方もなく汚い。僕ならこんなコードを書かされたら嫌になってくるだろうが、初期の Haskell のユーザも実際そう思っていたのだ。残念ながら、話は暗くなる一方だ。
Vector[String]
は標準出力ストリームならうまくいくかもしれないが、標準入力はどうすればいいだろう?一見すると、readLine
関数を変えるのは、何も変更しなくてもいいという点において、 println
関数よりも簡単にいくように見える。残念ながら、readLine
を繰り返して呼び出しても同じ戻り値が返ってこないように、明らかにどこかのレベルで何かが変わらなければいけない。
グラフィックの更新、ネットワーク、とリストは続く。実のところ、役に立つプログラムの全ては何らかの形で副作用を持たなければいけない。そうじゃないと、その役立つ結果が観測可能な形でプログラムの外に出すことができないからだ。そのため、Haskell の言語設計者(具体的には、Phillip Wadler)は標準出力だけじゃなく、全ての副作用をさばくことができる解決策を練る必要があった。
解決策は、実は結構単純なものだ。println
の問題を解決するためには、僕らは Vector[String]
を受け取り、新たな状態を通常の戻り値と一緒に渡すことで「変更」する必要があった。このアイディアを広げてみよう: もしこれを世界全体に適用したらどうだろう?ただの Vector[String]
の代わりに、Universe
を渡して回るのだ:
def foo(bar: String, everything: Universe) = {
val everything2 = println(bar, everything)
(bar.length, everything2)
}
def println(str: String, everything: Universe) = everything.println(str)
言語ランタイムが、役に立つ Universe
のインスタンスを提供するならば、このコードはうまくいくはずだ。当然、ランタイムは本気で宇宙全体をパッケージングして新たなバージョンを渡すことはできないが、少しズルをして世界全体を渡すフリをすることはできる。言語ランタイムは、println
関数を Universe
オブジェクトに対して、思いのまま実装することができる(願わくば、標準出力に文字を追加して欲しいところだが)。このようにして、ランタイムに必要に応じて何らかの魔法を実行させることで、僕らは全ての副作用に関して知らぬが仏を決め込むことができる。
これは確かに問題を解決することができるが、その一点以外のほぼ全ての点において最悪な解決策だ。まず、Universe
をいちいち渡して回る必要がある。これは手間だし冗長だ。さらに悪いことに、このようなものは往々にしてエラーが起きやすい。(例えば、世界全体を「変更」した後で、旧バージョンの世界全体を変更したとしたらどうなるだろう?二つのパラレルワールドが得られるのだろうか?)つまり、旧バージョンの世界から新バージョンの世界全体を計算する必要があり、それを手で行なっていることが問題の中心にある。何かを受け取り(この場合、世界全体)、その値を用いて新たな何か(新しいバージョンの世界全体)を計算している。聞いたことがあるよね?
Phillip Wadler のアイディアはモナドパターンを利用してこの問題を解決することだ。その結果が IO
モナドだ:
def foo(bar: String): IO[Int] = {
println(bar) bind { _ => IO(bar.length) }
}
def println(str: String): IO[Unit] = {
// TODO 魔法の呪文をここに書く
}
当然ながら、この仮想の言語は副作用を記述できないために println
を自分たちで実装することはできない。しかし、言語のランタイムは、副作用を実行してさらに新たなバージョンの IO
(つまり、変更された世界全体)を作成する println
のネイティブな(つまりズルをした)実装を提供することができる。
この設計で気をつけなければならないのは、IO
から何かを取り出すことができないということだ。一度、暗い道に入ってしまうと永遠に運命を支配してしまう。この理由として言語の純粋さが挙げられる。Haskell は、副作用を禁止したいが、IO
から値を取り出すことを許してしまうと、それを使って簡単に安全装置をすり抜けることができるからだ:
def readLine(): IO[String] = {
// TODO 魔法の呪文をここに書く
}
def fakeReadLine(str: String): String = {
val back: IO[String] = readLine()
back.get // whew! doesn't work
}
見ての通り、もし IO
から値を取り出すことができれば、ラッパー関数を使ってあまりにも簡単に副作用を隠すことができるので、この仕組全体が時間の無駄ということになってしまう。
実のところ、Java はもとより、Scala や Ruby にはこのことはあまり関係の無いことだ。Scala は副作用を制限しない。println
を好きなときに呼び出すことができるし、可変な変数を宣言する方法もある(var
)。ある意味では、Scala は仮想の IO
モナドを隠している。つまり、全ての Scala コードは暗黙に IO
の中にあって、暗黙に世界全体の状態を次々と渡していると考えることができる。この事実に照らして考えると、何故 IO
の仕組みを気にする必要はあるのだろう?大きな理由の一つに、これが Thing
や Option
と大きく異なるモナドであることが挙げられる。State
を解説に使うことも考えたが、「ある物の値から別の物を計算する」という中心的な考えに焦点を絞りたいのに、これには付随的な複雑さが多すぎた。
次は?
僕らはモナドパターンを特定することができた。コードの中から見つけ出して、驚くべき広範囲に適用できるようにもなったが、わざわざこんな儀式をするのが何の得になるのだろう?既に意識することなく、いたる所で(例、セミコロン)モナドを使っているなら、わざわざ用語を持ち出す必要があるだろうか?端的に言うと、「ちゃんと動く」なら Option
がモナドだと知って何の役に立つのだろう?
まず、第一の答は数学という学問、よってその延長としてのコンピュータプログラミングの性質に内在する。前にも言ったが、数学はパターンの特定が全てだ(それらのパターンをいじって、より大きく複雑なパターンを生成したりもするが、それは目的のための手段にすぎない)。パターンが特定できれば、それに名前を付ける。それが数学者の生きる道だ。ニュートンが「導関数」を命名せずに、「x が渡された線の式の変数である場合に、ある特定の値 x に対する渡された線の接線の式」と呼ぶと主張したと仮定しよう。第一に、微分積分の教科書は 50倍に膨れ上がる。第二に、僕らは「導関数」を抽象的な概念として捉えることができなかっただろう。偏微分は恐らく発明されなかっただろう。積分法、微分方程式、無限級数、そして物理学の全ては起こらなかったかもしれない。これらの結果ただのラベルである名前とは一切関係ない。そうではなく、ニュートンが導関数を抽象的なものとして見ることができて、それを操作可能な数学的な形として表して、新しい分野に応用したことに意味があるのだ。
Option
モナドが理解できれば、Option
モナドを使うことができる。それが適用できる所をコードから見つけて多大な利益を享受することができる。しかし、抽象的概念としてのモナドを理解できれば、Option
を理解できるだけでなく、State
、Parser
、STM
とリストは続く。少なくとも、これらの構造の基本的な特性を理解することができる(残りは些細なものだ)。Option
や State
の型にはまらないが、モナド的なことが行われいる所を見つけ始めるだろう。ここに真の効用がある。
考えのプロセスが(大きく)向上することの他に、より短期的で実践的な効果がある。あるモナドに特定するのではなく、ジェネリックにモナドに作用する関数を定義することができることだ。特定の Component
を加えるたびに全ての関数を書きなおしていては Swing のプログラミングが不可能なように、特定のモナドに対して関数を書きなおしていては不可能な(少なくとも、すごく非実用的である)ことが多くある。そのような関数の一つに sequence
がある:
trait Monad[+M[_]] {
def unit[A](a: A): M[A]
def bind[A, B](m: M[A])(f: A => M[B]): M[B]
}
implicit object ThingMonad extends Monad[Thing] {
def unit[A](a: A) = Thing(a)
def bind[A, B](thing: Thing[A])(f: A => Thing[B]) = thing bind f
}
implicit object OptionMonad extends Monad[Option] {
def unit[A](a: A) = Some(a)
def bind[A, B](opt: Option[A])(f: A => Option[B]) = opt bind f
}
def sequence[M[_], A](ms: List[M[A]])(implicit tc: Monad[M]) = {
ms.foldRight(tc.unit(List[A]())) { (m, acc) =>
tc.bind(m) { a => tc.bind(acc) { tail => tc.unit(a :: tail) } }
}
}
これを小ぎれいにする方法はいくらでもあるが、説明のために全てを明示的に書きだした。sequence
の一般的な機能は、モナドのインスタンスの List
を受け取り、それらの要素を含んだ List
のモナドを返すことだ。以下に具体例で説明する:
val nums = List(Some(1), Some(2), Some(3))
val nums2 = sequence(nums) // Some(List(1, 2, 3))
これはどのモナドにも適用できる:
val nums = List(Thing(1), Thing(2), Thing(3))
val nums2 = sequence(nums) // Thing(List(1, 2, 3))
この場合、Monad
トレイトは型クラスの一例だ。基本的には、モナドという一般的な概念があり、それは unit
と bind
という二つの関数を定義すると言っているだけだ。この仕組により、僕らはどの特定のモナドなのかを知らずにモナドの操作をすることができる。アダプターパターンをステロイド剤で強化した物(それに Scala の implicit の魔法を適量)だと考えればいい。
モナドの一般概念を理解するべき理由の二つ目は、突然便利な関数を大量に定義できるようになることだ。例としては、Haskell の標準ライブラリさえ見れば他は何も見なくてもいい。
やっかいなモナド則
おめでとう!モナド則が何なのかを心配したりその意味を考えること無くモナドのチュートリアルを終えることができた。考え方が分かった(大丈夫かな?)ところで、モナド則に進むことができる。
モナド則は、実際のところは結構直観的なものだ。今まで、ハッキリと言わずに当然の事として仮定してきたぐらいだ。この公理は unit
(コンストラクタ)と bind
(合成)関数が特定の状況においてどのように振る舞うかを規定する。整数の加算を司る法則(交換法則、結合法則、その他)に似ていると考えることもできる。モナドの全体像を語るものではない(直観的な立場からすると、何も語ってるとは言えないかもしれない)が、モナド則は基本的な数学的な土台を与えてくれる。
興味が湧いてきた?湧かないって?多分、そう思ったよ。でも、ここに前述の Monad
型クラスをを使って定義したものを書いておく:
def axioms[M[_]](implicit tc: Monad[M]) {
// 単位元律 1
def identity1[A, B](a: A, f: A => M[B]) {
val ma: M[A] = tc.unit(a)
assert(tc.bind(ma)(f) == f(a))
}
forAll { (x, f) => identity1(a, f) } // ScalaCheckっぽいもの
// 単位元律 2
def identity2[A](ma: M[A]) {
assert(tc.bind(ma)(tc.unit) == ma)
}
forAll { m => identity2(m) }
// 結合律
def associativity[A, B, C](m: M[A], f: A => M[B], g: B => M[C]) {
val mf: M[B] = tc.bind(m)(f)
val mg: M[C] = tc.bind(mf)(g)
val mg2: M[C] = tc.bind(m) { a =>
tc.bind(f(a))(g)
}
assert(mg == mg2)
}
forAll { (m, f, g) => associativity(m, f, g) }
}
最初の二つの公理(「単位元律」のやつ)は、基本的には unit
関数は、bind
関数に対して、単純なコンストラクタであると言っている。そのため、bind
がモナドを「分解」して、値を関数パラメータに渡すとき、それは unit
がモナドに格納する値と全く同じものだ。同様に、bind
に渡された関数パラメータが単に値をモナドで囲むものの場合、その結果は元のモナドに何もしなかったのと同値だ。
第三の公理は、表現は最も複雑だが、最も直観的なものだと思う。まず、最初にある関数と bind
して、その戻り値を別の関数と bind
した場合、それは第一の関数をモナドの中の値にまず適用して後で、その戻り値に bind
を呼び出したものを同値であると言っている。これは、古典的な意味での結合性とはちょっと違うが、それに似ていると考えることができる。
第二の公理による結果で便利なものの一つに、bind
を別の bind
の中に入れ子にした場合に使えるものがある。そのような状況に遭遇した場合、入れ子になった bind
は常に外側の bind
の外に出してコードをより平坦なものにすることができる。以下に具体例で説明する:
val opt: Option[String] = Some("string")
opt bind { str =>
val innerOpt = Some("Head: " + str)
innerOpt bind { str => Some(str + " :Tail") }
}
// これは、以下と同様だ
opt bind { str => Some("Head: " + str) } bind { str => Some(str + " :Tail") }
書きなおされたコードはより、「順次的 (sequential)」な感じがする(これがモナドの真髄だ!)し、通常入れ子構造になったものよりも短いものになる。
前述のとおり、モナド則はモナドの抽象的な概念を理解出来れば、とても、とても直観的なものだ。表現は直観的じゃないかもしれないが、導きだされる結果は理解しやすく、実践においても自然なものだ。だから、頑張って公理を暗記するようなことはしなくてもいい。セミコロンの仕組みについて頭をヒネった方が時間を有効に使えるだろう。
結論
モナドは怖くない。モナドは複雑でも、学術的でも、難解でもない。モナドは、ほぼ全てのコードに現れるパターンに付された抽象的な数学のラベルだ。僕らは日常的にモナドを使う。モナドの理解で最も難しい所は、最も難しい所がそんなに難しくないということに気づくことだろう。
モナド解説は、人通りの多い山道だが、僕のおぼつかない冒険が有益なものだったと誠意を持って希望する。モナドを理解し、完全に把握した後で生まれる視点は実践的で、泥臭いコーディングにおいても(例え旧態依然とした Java のような言語においてでも!)貴重なものだと躊躇することなく言うことができる。モナドは順次的な計算 (sequential computation) と合成可能性 (composability) に対する理解の骨組みを授けてくれる。もしそれが十分な動機にならないとしたら、僕には他に思いつくものがない。