Deletions are marked like this. | Additions are marked like this. |
Line 247: | Line 247: |
まず、ScalaのOptionのソースコードから関係ありそうな部分を抜き出すと以下になる。 日本語のコメントをつけてみた。 {{{ object Option { /** これがunit。Option(a)などと呼び出すのだが、aがnullであればNoneが返される */ def apply[A](x: A): Option[A] = if (x == null) None else Some(x) } sealed abstract class Option[+A] extends Product with Serializable { /** SomeかNoneかを判断するだけ */ def isEmpty: Boolean /** 中身を返す。Noneであれば例外 */ def get: A /** これがflatMap。自身がNoneであればfは呼び出さずにNoneを返す */ def flatMap[B](f: A => Option[B]): Option[B] = if (isEmpty) None else f(this.get) } /** 値を一個だけくるむコンテナ */ final case class Some[+A](x: A) extends Option[A] { def isEmpty = false def get = x } /** 何も持たないコンテナ */ case object None extends Option[Nothing] { def isEmpty = true def get = throw new NoSuchElementException("None.get") } }}} |
モナド
※以下は間違いの可能性があるので信用しないように。これは単なる学習メモです。
第一印象
モナドとは、連続して計算を行うパイプラインを簡単に作成するためのものである。古今東西同様の仕組みはいろいろ考えられてきたが、その一種と言える。例えば、
unixのパイプ
入力を受け取ってフィルタリングを行い出力をするコマンドをつなぎ合わせる。
ls -l | grep something | sort -k 6,6
Javaのストリーム
これも同じ。前のオブジェクトの入力を受け取り、フィルタリングを行う出力するオブジェクトを数珠つなぎにする。
BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream("something")));
形態は異なるけれどもやりたいことはこれと同じ。「任意の計算をつなぎあわせて所望の目的が達成できればとても便利だろう」という悲願の実現であると言える。
モナド則
「モナド」となるオブジェクトは「モナド則」を満たす必要があるという。モナド則とは以下のようなものだ。
- (1) m flatMap unit ≡ m
- (2) unit(x) flatMap f ≡ f(x)
(3) m flatMap g flatMap f ≡ m flatMap {x => g(x) flatMap f}
関数脳のつくり方 Second Season ~モナドで悟りをひらく~より
Haskellの本にも同じような記述がある(上と合うように順序を並べ替えてある)。
2. m >>= return == m
1. (return x) >>= f == f x
3. (m >>= f) >>= g == m >>= (\x -> f x >>= g)
「ふつうのHaskellプログラミング」(Softbank Creative)より
※ちなみに、先の「関数脳のつくり方」に「それらのメソッド(filter,map,flatMap)があったうえで,「モナド則」を満たす必要があります」とあるが、これは間違いではないだろうか?たしかに書籍「Scalaスケーラブルプログラミング」P438にも「あらゆるモナドは、map、flatMap、filterの3つ、さらに「ユニット」コンストラクターによって特徴づけられる」とあるが、少なくともこれらは厳密なモナドの定義とは無関係のようである。
Listがモナド則を満たすかを確認する
Listがこのとおりに動作するかを確認してみる。まず、unitは単なるコンストラクタ呼び出しであることに注意。
val m = List(1, 2, 3) def unit = (x: Int) => List(x) def f = (x: Int) => List(x * 3) def g = (x: Int) => List(x / 2)
※もちろんすべてのf,gについて確かめることは不可能なので、少なくともf,gの適用順序が変わると結果が変わるようにしとく。
※unitというのは、単なるコンストラクタ呼び出し(ほんとはコンストラクタではないのだが)。
すると、(1)は
m flatMap unit // List(1, 2, 3)になる
(2)は
unit(33) flatMap f // List(99)になる。 f(33) // List(99)になる。
(3)は
m flatMap g flatMap f // List(0, 3, 3)になる。 m flatMap {x => g(x) flatMap f} // List(0, 3, 3)になる
と、とりあえず一種類の関数についてListがモナド則を満たすことは確認できた。
モナド則の意味
さて、モナド則は何を意味しているのだろうか?日本語で説明して欲しいものである。 上のListの例に即して考えてみる。
(1) m flatMap unit ≡ m
これは簡単。Listのそれぞれの要素にコンストラクタを適用し、要素を一つだけ持つListを生成しても、それらはListから取り出され合体されて元通りListになる、ということである。
(2) unit(x) flatMap f ≡ f(x)
これも簡単。xを引数としてunitでモナドを作り、そのflatMapが動作するとき、fに与えられる引数はxである、ということ(たぶん)。
(3) m flatMap g flatMap f ≡ m flatMap {x => g(x) flatMap f}
これは結合則のようなものと思われる。左辺はListに対してflatMap gを適用した結果にflatMap fを適用しているが、 右辺では、個別の要素にgを適用した結果にflatMap fを適用する、という処理をListに適用している。
やはりよくわからないが、しかしこれらのモナド則に反する例を考えてみれば理解を深めることができるのではないか。 しかし、ちょっと考えてみれば、これらに反する例はむしろ「不自然」と言えるので、素直に実装すればモナド則に反することは無いような気がする。どうみても不自然でモナド則に反する例は以下のとおり、
- unitが引数を無視して無関係なものを返す。
- flatMapが引数の関数を呼び出した結果を無視したりデタラメに並び替える。
モナドを実装してみる
考えていてもわからないので、とりあえずモナドを実装してみる。 インチキだが、簡単のため、以下のようなコードをでっち上げる。 内部ではListを使っているが、当然ながら外側からはそれはわからない。
case class MyList[T](x: T*) { val list: List[T] = x.toList def flatMap(f: (T) => MyList[T]): MyList[T] = { def fResultToList(x: T): List[T] = { val fResult: MyList[T] = f(x) fResult.list } val newList: List[T] = list.flatMap(fResultToList) MyList(newList: _*) // 注:これはListを可変長引数に変換している。 } override def toString(): String = { list.map(_.toString()).mkString(" ") } }
これを以下のように呼び出す。
val m = new MyList(1, 2, 3) println(m) def unit = (x: Int) => MyList(x) def f = (x: Int) => MyList(x * 3) def g = (x: Int) => MyList(x / 2) println(m flatMap unit) println(unit(33) flatMap f) println(f(33)) println(m flatMap g flatMap f) println(m flatMap {x => g(x) flatMap f})
結果は以下のようになり、リストモナドの完成である(たぶん)。
1 2 3 1 2 3 99 99 0 3 3 0 3 3
ここまでの「推測」が間違っていなければ、モナドとは非常に単純なものであることがわかる。 おそらく、この単純なものを使って、難しいことをやろうとするから難しくなってしまうのだろう。 モナドを説明するサイトがいくつもあるが、例が難しすぎて(あるいはややこしすぎて)途中で面倒になってしまう。
Javaでモナド実装
これをJavaで無理やり実装してみる。ジェネリックスをもサポートしようとすると、かなり大変であることがわかる。 モナド自体の実装は大したことはないのだが、関数の代わりに匿名オブジェクトを作成しなければならない点がかなりめんどい。 わかりきったことをうだうだと書かなければならないので。
※ちなみに、だからといってスクリプト言語信者がJavaを批判するのは完全に間違い。いちいち型を書かなければならない代わりに、それらの言語には決して存在しようもない「安全性」が得られるのである。これらは言語の優劣ではなく、単純に何を重要視するかの問題でしかない。また、多くのスクリプト言語においてこのような安全性を求めるなら、かなり多くのユニットテストを記述しなければならないうえ(おそらくこのコードよりも大きくなるだろう)、それらは決してコンパイラによる機械的に検出可能な範囲には及ばない。
interface Function1<A1, R> { public R doit(A1 a1); } class OurList<T> { private java.util.List<T>list; public OurList(T...args) { list = java.util.Arrays.asList(args); } protected OurList(java.util.List<T>list) { this.list = list; } public OurList<T>flatMap(Function1<T, OurList<T>> func) { java.util.List<T>resultList = new ArrayList<T>(); for (T element: list) { OurList<T>newList = func.doit(element); resultList.addAll(newList.list); } return new OurList<T>(resultList); } @Override public String toString() { StringBuffer result = new StringBuffer(); for (T e: list) { result.append(e.toString() + " "); } return result.toString(); } } interface OurListFunc extends Function1<Integer, OurList<Integer>> { } public class MonadTest { public static void main(String[]args) { OurList<Integer> m = new OurList<Integer>(1, 2, 3); System.out.println(m); OurListFunc unit = new OurListFunc() { public OurList<Integer> doit(Integer x) { return new OurList<Integer>(x); } }; final OurListFunc f = new OurListFunc() { public OurList<Integer> doit(Integer x) { return new OurList<Integer>(x * 3); } }; final OurListFunc g = new OurListFunc() { public OurList<Integer> doit(Integer x) { return new OurList<Integer>(x / 2); } }; System.out.println(m.flatMap(unit)); System.out.println(unit.doit(33).flatMap(f)); System.out.println(f.doit(33)); System.out.println(m.flatMap(g).flatMap(f)); OurListFunc h = new OurListFunc() { public OurList<Integer> doit(Integer a) { return g.doit(a).flatMap(f); } }; System.out.println(m.flatMap(h)); } }
実行結果は当然以下
1 2 3 1 2 3 99 99 0 3 3 0 3 3
Option
さて、少なくとも私の理解が正しいのであれば、モナドとは何であるかが理解できたし、モナドの記述も極めて簡単であることがわかった。「この仕組みを使うと何がどのように便利になるのか」を知りたいところである。次はScalaに組み込みのOptionを見てみる。これもモナドだという。
ただし、単純にunit(コンストラクタ)やflatMapを使っただけでは、「ありがたみ」は全くない。最初に書いたように、それらを連続して使う、つまり計算をつなぎ合わせるところにモナドのありがたみがある、と予測している。
まず、ScalaのOptionのソースコードから関係ありそうな部分を抜き出すと以下になる。 日本語のコメントをつけてみた。
object Option { /** これがunit。Option(a)などと呼び出すのだが、aがnullであればNoneが返される */ def apply[A](x: A): Option[A] = if (x == null) None else Some(x) } sealed abstract class Option[+A] extends Product with Serializable { /** SomeかNoneかを判断するだけ */ def isEmpty: Boolean /** 中身を返す。Noneであれば例外 */ def get: A /** これがflatMap。自身がNoneであればfは呼び出さずにNoneを返す */ def flatMap[B](f: A => Option[B]): Option[B] = if (isEmpty) None else f(this.get) } /** 値を一個だけくるむコンテナ */ final case class Some[+A](x: A) extends Option[A] { def isEmpty = false def get = x } /** 何も持たないコンテナ */ case object None extends Option[Nothing] { def isEmpty = true def get = throw new NoSuchElementException("None.get") }