search term:

モナドはメタファーではない

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
  }
}

またしても、ありがちなパターンだ。ここに二つの関数 (firstNamelastName) があり、それぞれ利用可能か不可能か分からないデータを取得する。もしデータがあれば、その値が返る。それ以外の場合は、null を返す。次に、これらの関数を使って何か面白いことをする(この場合、フルネームを計算する)。残念ながら firstNamelastName が役に立つ値を返すか返さないのかは明示的に入れ子になった 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 だ。NoneThing(null) と簡単に書く方法だと考えればいい。

面白いのは、SomeNone で二つの異なる bind の定義が必要なことだ。Some の中の bind の定義は Thing のものにそっくりだ。これは、SomeThing がほぼ同一なことで説明がつく。しかし、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 文が無くなった。これは firstNamelastName の両者ともがデータベースのレコードの取得に失敗すると Thing(null) の代わりに None を返すために機能する。もちろん、None に対して bind しようとしても、戻り値は常に None だ。よって、fullName 関数は、firstNamelastName の両者共のが None では無い時に値の組み合わせを Some に入れて返す。

ご家庭で得点をつけている皆様、僕らは「偶然」にも Groovy の安全な参照演算子 (?.)、Raganwald の Ruby のための andand など、などを発見しました。どうだろう?モナドはいたる所にある。

IO

モナドを理解しようとすると、いずれぶつかる壁に Haskell の IO モナドがあり、結果はいつも同じだ: 狼狽、混乱、激怒、そして最終的には Perl。実際の所は、IO はモナドの中では変わり者なのだ。基本的には ThingOption と同じカテゴリーにあるのだが、全く異なる問題を解決する。

説明しよう。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 の仕組みを気にする必要はあるのだろう?大きな理由の一つに、これが ThingOption と大きく異なるモナドであることが挙げられる。State を解説に使うことも考えたが、「ある物の値から別の物を計算する」という中心的な考えに焦点を絞りたいのに、これには付随的な複雑さが多すぎた。

次は?

僕らはモナドパターンを特定することができた。コードの中から見つけ出して、驚くべき広範囲に適用できるようにもなったが、わざわざこんな儀式をするのが何の得になるのだろう?既に意識することなく、いたる所で(例、セミコロン)モナドを使っているなら、わざわざ用語を持ち出す必要があるだろうか?端的に言うと、「ちゃんと動く」なら Option がモナドだと知って何の役に立つのだろう?

まず、第一の答は数学という学問、よってその延長としてのコンピュータプログラミングの性質に内在する。前にも言ったが、数学はパターンの特定が全てだ(それらのパターンをいじって、より大きく複雑なパターンを生成したりもするが、それは目的のための手段にすぎない)。パターンが特定できれば、それに名前を付ける。それが数学者の生きる道だ。ニュートンが「導関数」を命名せずに、「x が渡された線の式の変数である場合に、ある特定の値 x に対する渡された線の接線の式」と呼ぶと主張したと仮定しよう。第一に、微分積分の教科書は 50倍に膨れ上がる。第二に、僕らは「導関数」を抽象的な概念として捉えることができなかっただろう。偏微分は恐らく発明されなかっただろう。積分法、微分方程式、無限級数、そして物理学の全ては起こらなかったかもしれない。これらの結果ただのラベルである名前とは一切関係ない。そうではなく、ニュートンが導関数を抽象的なものとして見ることができて、それを操作可能な数学的な形として表して、新しい分野に応用したことに意味があるのだ。

Option モナドが理解できれば、Option モナドを使うことができる。それが適用できる所をコードから見つけて多大な利益を享受することができる。しかし、抽象的概念としてのモナドを理解できれば、Option を理解できるだけでなく、StateParserSTM とリストは続く。少なくとも、これらの構造の基本的な特性を理解することができる(残りは些細なものだ)。OptionState の型にはまらないが、モナド的なことが行われいる所を見つけ始めるだろう。ここに真の効用がある。

考えのプロセスが(大きく)向上することの他に、より短期的で実践的な効果がある。あるモナドに特定するのではなく、ジェネリックにモナドに作用する関数を定義することができることだ。特定の 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 トレイトは型クラスの一例だ。基本的には、モナドという一般的な概念があり、それは unitbind という二つの関数を定義すると言っているだけだ。この仕組により、僕らはどの特定のモナドなのかを知らずにモナドの操作をすることができる。アダプターパターンをステロイド剤で強化した物(それに 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) に対する理解の骨組みを授けてくれる。もしそれが十分な動機にならないとしたら、僕には他に思いつくものがない。