JavaでMonadをはじめからていねいに
by Yuji Yamamoto on August 28, 2016
「モナドについてSwiftで説明してみた」という記事などで指摘されているように、プログラマー向けにMonadを説明した記事はサンプルがHaskellで書かれていることが多いので辛いですよね。
HaskellではMonadは「ないと文字通りプログラムが書けないぐらい」大事なもので、入出力処理や例外処理、ダイナミックスコープのシミュレーション、非決定計算など、普通の関数では難しい、非常に様々な機能の実装に使用されています。
その一方、MonadはC言語のポインターに並んでHaskellを学ぶ上での障害である、なんて言われたりもするとおり、他言語ユーザーからHaskellを敬遠させる大きな要因の一つともなっています。
そんな悲しい現状を少しでも改善するために、上記の記事を参考にしつつ、HaskellよりもSwiftよりももっともっと広まっているであろう、Java (Java 8以降の知識を前提とします)でMonadを定義して説明してみたいと思います 1。
これを通して、
- Monadの何が嬉しいのか
- よく聞く「Monad則」ってどんなやつなのか、何が嬉しいのか
- do記法がどんなことをしているのか
と言った点も説明したいと思います!私自身Javaを勉強中なんで一石二鳥ですね!
あっ、「Java 8以降の知識を前提とします」とは言いましたが、ほかの似たような言語を知っていれば大体わかるようには書いたつもりなので、わかりにくいという場合はご連絡ください。
このページの一番下までスクロールして、私にメールかIssueを送っていただけるとありがたいです。
あるいはそんなのめんどくさいよ、という場合でもご安心ください。この文章はどちらかというと「Monadの詳しい仕組みを知りたい人向け」なので、書いてあることがわからなくても、とりあえずMonadを使うことはできるはずですから。
最初にまとめ (ここ以降がよくわかんなくても何となく知ってもらいたいこと)
- MonadをJavaで表現すると、インターフェースで表現することができる。
- do記法があるおかげで、HaskellではMonadが思いの外役に立つ。
- do記法をdo記法らしく直感的に使えるようにするためには、Monadインターフェースを実装したクラスが、Monad則というルールを守る必要がある。
- Monadは各文と文を繋ぐものの意味を変える、すなわち、(
+
などの演算子と同様に)セミコロンをオーバーロードできるようにしてくれる。
目次
とりあえずJavaでの定義を。
手始めに、Monadの定義をJavaに翻訳してみましょう 2。 Haskellや圏論での呼び方も、より分かりやすくなるようもっと具体的な名前に差し替えます。
/**
* 本当は一つのインターフェースにまとめたいのですが、
* Javaのインターフェースの都合上やむを得ず分けています。
* 本来は2つ合わせて初めてMonadと呼べる、という点をお忘れなく。
*/
interface Monad<T1> /* (1) */{
<T2> Monad<T2> then(Function<T1, Monad<T2>> action /* (2) */);
interface Return {
/* (3) */
<T> Monad<T> doNothingReturning(T value);
}
}
Java 8がある程度普及した現在、関数を受けとる関数を表現するのがらくちんになりましたね!
番号を振ったところを解説しましょう。
- (1): 型引数を1つとります。なので、渡した型をラップする型となります。
- (2): 引数として、「ラップした型T1を受け取り、別の型T2をラップしたMonadを返す関数」を受け取ります。
- 戻り値は、そのT2をラップしたMonadでなければなりません。
- (3): 任意の値一つを受け取って3、そのMonadのインスタンスを返す、ファクトリーメソッド的なものが必要です。
Java固有の情報を抜いて、もうちょっとオブジェクト指向な言語全般で通じそうな日本語で言うと、次のように言い換えられます。
Monad interfaceを実装したクラスは、
- ほかの型をラップする型でなければならない。
then
というメソッドを実装しなければならない。then
は別のMonadのインスタンスを返す関数を受け取って、その関数が返す値と同じ型の値を返さなければならない。doNothingReturning
というファクトリーメソッドを実装しなければならない。- 任意のクラスの値を1つ受け取って、そのインスタンスを作れなければならない。
いろいろ書きましたが、ここで特に大事なところは「then
というメソッドを実装しなければならない」と「doNothingReturning
を実装しなければならない」の二点です!
具体的な実装その1 Maybe
重要な注意事項:
次の例を含め、この記事で紹介するMonad
インターフェースを実装したクラスは、残念ながら全てコンパイルが通りません。Javaのインターフェースの仕様上仕方ないのです。
あくまでも説明のためのコードだということでご了承下さい。
ダウンキャストなどを使って一応コンパイルを通したソースがigrep/monad-in-java-sampleにあります。
また、もっとちゃんとしたMonadのJavaによる実装に興味がある方はhighjをご覧下さい(結構トリッキーな実装なので、今回の説明では使用しませんでした)。
重要な注意事項終わり
さて、それではMonadについてもうちょっと具体的なイメージを持っていただくために、前節で定義したMonad
インターフェース(と、対応するMonad.Return
インターフェース)を実装したクラスの例を紹介しましょう。
class Maybe<T1> implements Monad<T1> {
private final T1 x;
Maybe(T1 x){
this.x = x;
}
public <T2> Maybe<T2> then(Function<T1, Maybe<T2>> nextAction){
if (x != null){
return nextAction.apply(x);
} else {
return new Maybe<>(null);
}
}
public static class Return implements Monad.Return {
public <T> Maybe<T> doNothingReturning(T x){
return new Maybe<>(x);
}
}
}
上記はthen
において、持っている値(x
)がnull
かどうかあらかじめ確認し、null
でなければ引数として渡した関数(nextAction
)を実行します。
次の関数(nextAction
)を実行する前に「null
でないか確認する」のが上記のMaybe
クラスのthen
の役割です。
この後のMonad則の説明でも触れますが、「Monadの値を作って返すだけで、それ以外のことはしない」のがdoNothingReturning
のお約束なので、Monadインターフェースを実装するクラスをより明確に特徴付けるのは、then
メソッドの方です。
そのため繰り返しになりますが、次の関数(nextAction
)を実行する前に「null
でないか確認する」のが上記のMaybe
クラスの役割だ、と言い換えることもできるでしょう。
ところで、最近のJavaに慣れた方はMaybe
がJava 8のOptional
とそっくりなものであることにお気づきかもしれません。
Maybe.then
はOptional.flatMap
に相当し、Maybe.Return.doNothingReturning
はOptional.ofNullable
に相当する、と考えると、確かに!
事実、その前提でopen-jdkのOptional.java
と見比べてみると、上記はOptional.java
を大幅に簡略化したものと同等であることがわかります 4。
Optional
と同じようなものなので特に目新しい部分はないかもしれませんが、上記のMaybe
の使用例を示しましょう。
= someMap.get(key1);
Foo maybeFoo1 .Return r = new Maybe.Return();
Maybe<Bar> bar =
Maybe.doNothingReturning(maybeFoo1).then((foo1) -> {
r= someMap.get(key2);
Foo maybeFoo2 return r.doNothingReturning(maybeFoo2).then((foo2) -> {
= someMap.get(key3);
Foo maybeFoo3 return r.doNothingReturning(maybeFoo3).then((foo3) -> {
return r.doNothingReturning(foo1.doSomething(foo2, foo3));
});
});
});
上記のように、null
を返すかもしれないメソッドをたくさん使うとき、Maybe
を使えば、then
にnull
チェックをお任せすることができます。
これでぬるぽともお別れだー!やったね!(^_-)
…と、言いたいところですが、元ネタのOptional.flatMap
を使った場合と同様、ネストが深くって嫌ですねー。
これでは普通にif文を書いた方がまだ読みやすそうです。r.doNothingReturning
なんて書く分タイプ数も多いですし。
r.doNothingReturning
やMaybe.Return r = new Maybe.Return();
の部分が長いのは単なる名付け方の問題だから(もっと短い名前を普及させればよいのであって)目をつぶるとして、結局ネストが深くなってしまう問題はどうにかならないのでしょうか?
実はこれから説明する「Monad則」というのを利用すると、このような問題をクールに解決することができます!
Monad則って?
冒頭の通り、Monad
インターフェースはthen
メソッドとMonad.Return
のdoNothingReturning
メソッド、合わせて2つのメソッドを実装しなければなりません。
加えて、これらのメソッドは「Monad則」というある一定の規則を満たして実装しなければならないよう決まっています。
ここではこの「Monad則」についてもJavaに置き換えて紹介しましょう。
詳細は後で解説するので、ここではひとまず重要な部分のみチラ見せします↓。
// 下記の3組の式が**常に同じ意味となる**doNothingReturningとthen
// となっていなければ、thenとdoNothingReturningを実装していても、
// 「そのクラスはMonadである」とはいえません。
/* (A ) */ r.doNothingReturning(x).then((x) -> ax.apply(x))
/* ↑と↓の意味が同じになること。以下同様。 */
.apply(x)
ax
/* (A') */ ax.apply(x).then((y) -> r.doNothingReturning(y))
/* ↑と↓の意味が同じになること */
.apply(x)
ax
/* (B ) */ ax.apply(x).then((y) -> ay.apply(y).then((z) -> az.apply(z)))
/* ↑と↓の意味が同じになること */
.apply(x).then((y) -> ay.apply(y)).then((z) -> az.apply(z)) ax
なんだかややこしいですね!
なんでそんなのがあるの?
そもそも、なぜこんな面倒な規則があるのでしょう?
この法則を満たすとMonadは非常に便利に使えるようになるのですが、それについては後ほど説明します。
その代わり、ここではJavaで言うところの似たような「規則」を紹介することで、「Monad則」の「立ち位置」みたいなものをお伝えしたいと思います。
例えばJavaな方にはお馴染みComparable
インターフェースを実装したクラスは、以下のように振る舞うよう定められています。
x.compareTo(y)
というメソッドを呼んだとき、
x
よりy
の方が「より大きい」時、0
より大きい値(1
にすることが多い)を返す。x
よりy
の方が「より小さい」時、0
より小さい値(-1
にすることが多い)を返す。x
とy
が「等しい」時、0
を返す。
これによって、ユーザーが自由に「全順序性」を持ったクラスを定義して、比較したりソートしたりできるようになるのでした。
ちなみに、RubyやPerlのスペースシップ演算子(<=>
)やPythonの(__cmp__
)など、そっくりな振る舞いを要求するメソッドは他の言語にもしばしばあります。
以下の話はそれらに置き換えて読んでいただいても全く問題ありません。
そんなComparable
ですが、上記の規則を破って、下記のように振る舞うComparable
があったら嫌ですよね?
/**
* 引数とレシーバーを逆にしても1(より大きい)が返る!
* どっちが本当に大きいの?
*/
.compareTo(y) // => 1
x.compareTo(x) // => 1
y
/**
* 自分自身と比較しても-1(より小さい)が返る!
* どうやったら等しくなるんだ!?
*/
.compareTo(z) // => -1 z
継承の仕方などによっては、間違って上記のようなおかしな振る舞いのComparable
を産み出してしまうことはあるでしょう。もちろん意図的にそうした実装を作ることも可能です。
このように不自然なComparable
を作らないためには、Comparable
を実装する側が意識しなければなりません。間違った実装を作ってしまっても、コンパイラーが「間違ってるよ!」と教えてくれないのです。
Monad則もこれと同じようなものです。
Monadはそもそも圏論という数学の一分野で定義されたものなので、その定義に準拠したものであるためには、単純にMonad
というインターフェースを実装するだけでなく、それに沿うよう気を付けて中身(then
とdoNothingReturning
)を実装しなければなりません。
そして、繰り返しますがこのMonad則を満たすからこそみなさんはMonadを便利に使えるのです!(詳細は後述!)
で、Monad則ってどんなのさ?
これ以降を読む上でのヒント: Java 8におけるFunction
型のオブジェクトは、apply
メソッドを呼ぶことで実行できます。
前置きが長くなってしまいましたが、いよいよ「Monad則」をJavaで紹介しましょう!
先ほどは省略した変数の定義も含めて、下記の通りです 5!
// 任意の型X, Y, Zの値をx, y, zとします。
;
X x;
Y y;
Z z
// Monad.Returnをrとします
.Return r = new SomeMonad.Return();
Monad
// それから、X, Y, Zを受け取って別のMonadの値を返す関数をax, ay, azとしましょう。
<X, Monad<Y>> ax = (x) -> ... ;
Function<Y, Monad<Z>> ay = (y) -> ... ;
Function<Z, Monad<A>> az = (z) -> ... ;
Function
// 下記の3組の式が**常に同じ意味となる**doNothingReturningとthen
// となっていなければ、thenとdoNothingReturningを実装していても、
// 「そのクラスはMonadである」とはいえません。
/* (A ) */ r.doNothingReturning(x).then((x) -> ax.apply(x))
/* ↑と↓の意味が同じになること。以下同様。 */
.apply(x)
ax
/* (A') */ ax.apply(x).then((y) -> r.doNothingReturning(y))
/* ↑と↓の意味が同じになること */
.apply(x)
ax
/* (B ) */ ax.apply(x).then((y) -> ay.apply(y).then((z) -> az.apply(z)))
/* ↑と↓の意味が同じになること */
.apply(x).then((y) -> ay.apply(y)).then((z) -> az.apply(z)) ax
各変数の定義まで書いてしまったので長ったらしくなってしまいましたが、一つずつ解説しましょう。
(A), (A’)について
最初の2つは、どちらかというとdoNothingReturning
が守るべき性質についての式です。
第一に(A)では、x
に対して「なにもしないで」Monadの値を作って(doNothingReturning(x)
)、それから(then
)、更にMonadを返す関数ax
を実行する(ax.apply
)ということは、単にax
を1回実行するのと同じことだ
— つまり、doNothingReturning
をどんなax
の前に実行しても、ax
を実行する(ax.apply(x)
)のと同じ、なのでdoNothingReturning
はなにもしないのと同等だ —
ということです。
(A’)についても同様です。
x
を受け取ってMonadを返す関数ax
を実行(ax.apply(x)
)して、それから(then
)、「なにもしないで」Monadの値を作る(doNothingReturning
)ことは、単にax
を1回実行するのと同じことだ
— つまり、doNothingReturning
をどんなax
の後に実行しても、ax
を実行するのと同じ、なのでdoNothingReturning
はなにもしないのと同等だ —
ということです。
いずれにおいても、doNothingReturning
はMonadの値を作って返すだけで、実質なにもしないという点が、ここでは重要です。
「そんなの役に立つの?」と思われるかもしれません。
とりあえずは「定義上そう決まっているのでそういうものだ」とご理解しておいてください。ここから先の例でなんとなく伝われば幸いです。
(B)について
(B)はちょっと複雑ですね。
集中するために該当の部分だけ持ってきましょう。
.apply(x).then((y) -> ay.apply(y).then((z) -> az.apply(z)))
ax/* ↑と↓の意味が同じになること */
.apply(x).then((y) -> ay.apply(y)).then((z) -> az.apply(z)) ax
ぱっと見どこが違うのかわかりませんね!違うのは↓に示す部分だけです!
ax.apply(x).then((y) -> ay.apply(y).then((z) -> az.apply(z)))
^ この.then((z) -> az.apply(z))が...
ax.apply(x).then((y) -> ay.apply(y)).then((z) -> az.apply(z))
^ カッコ()を突き破って、ここに出された!
上記の通り、二つめのthen
、すなわちay.apply
の後ろにあるthen((z) -> az.apply(z))
が、一つめのthen
、つまりax.apply(x).then
に渡したラムダ式から、括り出せるようになっていなければならない、ということです。
この規則によって、ネストを一段平たくできます。
then()
のカッコの中にインデントを加えてみるとよくわかるでしょう。
.apply(x).then(
ax(y) -> ay.apply(y).then(
(z) -> az.apply(z)
)
)
/* ↑と↓の意味が同じになること */
.apply(x).then(
ax(y) -> ay.apply(y)
).then(
(z) -> az.apply(z)
)
さて、この規則をもうちょっとくだけた日本語に言い換えると、
- 「
ax.apply(x)
してから『ay.apply(y)
してからaz.apply(z)
する』」ことと、 - 「
ax.apply(x)
してから『ay.apply(y)
してから』『az.apply(z)
する』」ことが、
常に同じ意味でないといけない、と解釈できます。
この解釈だと「そんなの当たり前じゃないの?」と感じられるかもしれません。
それぐらい直感的な仕様を守ってくださいね、というのが「Monad則」の正体だ、と考えていただければしっくりくるでしょうか?
Comparable
なx
が常にx.compareTo(x) == 0
であってほしいのと同じことです。
「Monad則」(B)のMaybeへの応用
さて、Monad則の話が長くなってしまいましたが、Maybeのお話に帰りましょう。
下記のようなMaybe Monadの例においてMonad則を応用すると、「ネストが深くって嫌ですねー」という問題をクールに解決することができる、というお話でした。
= someMap.get(key1);
Foo maybeFoo1 .Return r = new Maybe.Return();
Maybe<Bar> bar =
Maybe.doNothingReturning(maybeFoo1).then((foo1) -> {
r= someMap.get(key2);
Foo maybeFoo2 return r.doNothingReturning(maybeFoo2).then((foo2) -> {
= someMap.get(key3);
Foo maybeFoo3 return r.doNothingReturning(maybeFoo3).then((foo3) -> {
.doNothingReturning(foo1.doSomething(foo2, foo3));
r});
});
});
解決するヒントとして、「Monad則」の(B)を思い出してみましょう。
ax.apply(x).then((y) -> ay.apply(y).then((z) -> az.apply(az)))
ax.apply(x).then((y) -> ay.apply(y)).then((z) -> az.apply(az))
この規則は上の図のように、二つめのthen
、すなわちay.apply
の後ろにあるthen((z) -> az.apply(z))
を、一つめのthen
、つまりax.apply(x).then
に渡したラムダ式からくくり出すことで、ネストを一段平たくすることができる、というものでした。
おっ。ということは今回のケースにも適用できるかもしれませんよ!やってみましょう!
;
Foo maybeFoo1, Foo foo2, Foo foo3;
Foo foo1.Return r = new Maybe.Return();
Maybe
= someMap.get(key1);
maybeFoo1 <Bar> bar =
Maybe.doNothingReturning(maybeFoo1).then((foo1Arg) -> {
r= foo1Arg;
foo1 return r.doNothingReturning(someMap.get(key2));
}).then((foo2Arg) -> { // <- このラムダ式と、
= foo2Arg;
foo2 return r.doNothingReturning(someMap.get(key3));
}).then((foo3Arg) -> { // <- このラムダ式がくくりだされた。
= foo3Arg;
foo3 return r.doNothingReturning(foo1.doSomething(foo2, foo3));
});
よし、これならネストが減ってちょっと見やすくなった…? と、思いきや、今度はコンパイルエラーです… (>_<)
Java 8のラムダ式は、ラムダ式の中でローカル変数を書き換えることができないのでしたorz
残念ながらこの問題は、現在のJavaではどうしようもありません。
しかもいずれにしてもいちいちfoo1 = foo1Arg
みたいな代入が必要だったりで、結局面倒くさいですよね。
実はHaskellであれば、上記のような書き換えを自動で行い、自然な見た目にしてくれるシンタックスシュガー(糖衣構文)があります。
それが次に示す「do記法」と呼ばれるものです。
do記法
本記事はあくまでもJavaでMonadを説明する記事ですので、ここではその「do記法」を仮にJavaに導入した場合を想像して、先程の例がどのように書き換えられるか示しましょう。
.Return r = new Maybe.Return();
Maybe
<Bar> bar = do {
Maybe<- r.doNothingReturning(someMap.get(key1));
Foo foo1 <- r.doNothingReturning(someMap.get(key2));
Foo foo2 <- r.doNothingReturning(someMap.get(key3));
Foo foo3 .doNothingReturning(foo1.doSomething(foo2, foo3));
r};
おお、doNothingReturning
という長ったらしい名前さえどうにかなれば、もはや普通のJavaと区別がつかないくらい自然じゃありませんか!
「一体どこがどう糖衣構文で簡略化されたんだ?」という疑問にお答えしましょう。
この場合、元のコードのthen
メソッドの呼び出しが、細い矢印 <-
の箇所に置き換わったと考えると分かりやすいでしょう。
then
メソッドが「引数として渡された関数」に渡す引数(上記の場合foo1
, foo2
, foo3
)についてnull
かどうか確認していたのを、do記法では細い矢印 <-
でfoo1
, foo2
, foo3
に代入する前にnull
かどうか確認するようになったのです。
このような自動的な書き換えがあるからこそ、HaskellではMonadが思いの外便利に使えるのです。どうです?Javaにもちょっと欲しくなりませんか?
do記法とMonad則 (B)
ついでに、この「do記法」とMonad則(B)ののっぴきならない関係を示すことで、Monad則(B)を守ることの重要性についてもお話ししましょう。
復習のためにもう一度↓に持ってきました。
.apply(x).then((y) -> ay.apply(y).then((z) -> az.apply(z)))
ax/* ↑と↓の意味が同じになること */
.apply(x).then((y) -> ay.apply(y)).then((z) -> az.apply(z)) ax
上記の通りMonad則(B)は、二つめのthen
の引数におけるthen((z) -> az.apply(z))
の部分が、一つめのthen
、つまりax.apply(x).then
に渡したラムダ式から、処理の意味を変えずに、括り出せるようになっていなければならない、ということでした。
これによってネストを一段平たくできるのでしたね。
この規則は、先程のdo記法を用いると、次のように表現することもできます。
do {
y <- ax.apply(x);
do { // このdoブロック
z <- ay.apply(y);
az.apply(z);
};
};
// このdoブロック
と記されたdoブロックを引き剥がして、
do {
y <- ax.apply(x);
z <- ay.apply(y);
az.apply(z);
};
と必ず(処理の意味を変えずに)書き換えられなければならない、ということです。
あるいは逆に、doブロックの範囲を上にずらして、
do {
z <- do {
y <- ax.apply(x);
ay.apply(y);
};
az.apply(z);
};
のようにも書き換えられなければならない、とも言えます。
普通こんな無意味な書き換えはしないだろ、と思われるかもしれません。
ところがリファクタリングしたくなったときなど、do記法で並べた各行を、他のメソッドとして切り出したくなったときはいかがでしょう?
先程の例で申しますと、
do {
y <- ax.apply(x);
z <- ay.apply(y);
az.apply(z);
};
↑の色を塗った箇所だけ切り出して
public SomeMonad<Z> extractedMethod(X x){
return do {
y <- ax.apply(x);
ay.apply(y);
};
}
do {
z <- extractedMethod(x);
az.apply(z);
};
と書きたくなったり、
あるいは、
do {
y <- ax.apply(x);
z <- ay.apply(y);
az.apply(z);
};
から、色を塗った箇所だけを切り出して
public SomeMonad<A> extractedMethod(Y y){
return do {
z <- ax.apply(y);
az.apply(z);
};
}
do {
y <- ax.apply(x);
extractedMethod(y);
};
と書きたくなるかもしれません。
このように、do記法のどんなところから切り出しを行っても意味が変わらないようにするには、Monad則(B)を満たして、入れ子を関係を気にしなくてもいいようにすることが、必要不可欠なのです。
ここまでのまとめ
長くなりましたがここまでのまとめです。
- MonadをJavaで表現すると、
then
というメソッドを持ったインターフェースと、それに対応する、doNothingReturning
というメソッドを持ったファクトリークラスのインターフェースで表現することができる。 - それらのインターフェースを実装するクラスは、「Monad則」を満たすように
then
とdoNothingReturning
を実装しなければならない。- Monad則を守ることによって、do記法はdo記法らしく直感的に使えるようになる。
- そのdo記法があるおかげで、HaskellではMonadが思いの外役に立つ。
- Monadインターフェースを実装するクラスの振る舞いをより明確に特徴付けるのは、
Monad.Return.doNothingReturning
ではなくthen
メソッド。 Maybe
のthen
は、「次の関数を実行する前に値がnull
でないか確認」する。
具体的な実装 その2 State
さて、do記法のすごさを分かっていただけたところ(まだわからない場合、このページの一番下までスクロールして、私にメールかIssueを送ってください!)で、Monadの別の例を紹介しましょう。
今度はJavaではあまり役に立たないとは思いますが、純粋な関数のみを使っているのに、あたかも命令型スタイルで書かれているかのように見せる、魔法のようなMonadです。
ちょっとMonadじゃないクラスがいくつか出てきますが、Stateモナドが依存しているので、どうかご了承下さい。
もちろん後で解説しますので…。
// 状態を書き換えた結果を表すValue Object
class MutationResult<T, S> {
public final S newState;
public final T value;
MutationResult(S newState, T value){
this.newState = newState;
this.value = value;
}
}
// Stateモナドの実装
class State<S, T1> implements Monad<T1> {
// 状態の書き換えをシミュレートするための関数オブジェクト
public final Function<S, MutationResult<T1, S>> mutator;
State(Function<S, MutationResult<T1, S>> mutator){
this.mutator = mutator;
}
public <T2> State<S, T2> then(Function<T1, State<S, T2>> nextAction){
<S, MutationResult<T2, S>> composedMutator = (oldState) -> {
Function<T1, S> result = this.mutator.apply(oldState);
MutationResultState<S, T2> other = nextAction.apply(result.value);
return other.mutator.apply(result.newState);
};
return new State<>(composedMutator);
}
public static class Return<S> implements Monad.Return {
public <T> State<S, T> doNothingReturning(T value){
return new State<>(
(nonMutatedState) -> new MutationResult<>(nonMutatedState, value)
);
}
}
}
うーん、肝心のStateクラス以外のものがあったり、その上結構煩雑ですね…(^-^;
これだけでは何のこっちゃと感じられる方も多いと思うので、ちょっとずつ解説しましょう。
そもそも、「純粋な関数」のみで命令型スタイルに見せる、とは?
ここで言う「命令型スタイル」というのが「状態を書き換えることで結果を作る」プログラミングスタイルだとして、いわゆる「関数型プログラミング」 — つまり純粋な関数のみを使用したスタイルでは、どうやって状態の書き換えを表現するのでしょう?
純粋な関数は、関数が返した値を使用しない限り、関数の外部へ影響を与えることができません。ということで素直に「古い状態を受け取って、新しい状態を返す」関数として表現してみましょう。
JavaのFunction
で言うと↓のような感じ。
<S, S> mutator; Function
しかしこれでは、「状態を書き換えたと同時に、別の値も返したい!」という時に不便ですよね。
例えば、JavaのMap
のremove
メソッドは、呼び出し元のMap
の要素を取り除くことで状態を書き換えると同時に、取り除いた要素を返しますよね。
そうした振る舞いをシミュレートするためには、「古い状態を受け取って、新しい状態を返す」だけでなく、「一緒に返す別の値」も返せるようにする必要があるのです。
と、いうわけで出来たのが先ほど挙げましたMutationResult
とFunction<S, MutationResult<T1, S>> mutator
です。
↓にもう一度載せておきます。
// 状態を書き換えた結果を表すValue Object
class MutationResult<T, S> {
public final S newState; // 新しい状態
public final T value; // 一緒に返す値
MutationResult(S newState, T value){
this.newState = newState;
this.value = value;
}
}
class State<S, T1> implements Monad<T1> {
// 状態の書き換えをシミュレートするための関数オブジェクト
// Sという型の新しい状態とともに、Tという型の書き換えた結果も返す
public final Function<S, MutationResult<T1, S>> mutator;
}
「状態を書き換えたと同時に、別の値も返したい!」というニーズを満たすため、「新しい状態」と「一緒に返す別の値」とのペアを表すクラス MutationResult
を作りました。Function<S, MutationResult<T1, S>>
は古い状態を受け取って、MutationResult
のインスタンスを返すだけです。
なお、これ以降Function<S, MutationResult<T1, S>>
のような型の関数を、単純に「S
とT1
のmutator」と呼ぶことにします。Function<S, MutationResult<T1, S>>
とか「古い状態を受け取って、新しい状態と一緒に別の値を返す関数」では単純に長いので。
State
のコンストラクターがやっていること
class State<S, T1> implements Monad<T1> {
public final Function<S, MutationResult<T1, S>> mutator;
State(Function<S, MutationResult<T1, S>> mutator){
this.mutator = mutator;
}
// ...
}
コンストラクターでやっていることは、単に引数をインスタンス変数にいれるだけのボイラープレートなコードです。
ここで注目していただきたいのは、State<S, T1>
は、Function<S, MutationResult<T1, S>>
、すなわち「S
とT1
のmutator」をラップしただけのクラスであることです。
State
Monadはこれにちょっと色をつけるだけで、あたかも命令型スタイルで書いているかのように錯覚させることができるのです!
State.Return.doNothingReturning
がやっていること
難しいState.then
は後回しにして、先にdoNothingReturning
を解説しましょう。
class State<S, T1> implements Monad<T1> {
// ...
class Return<S> implements Monad.Return {
public <T> State<S, T> doNothingReturning(T value){
return new State<>(
(nonMutatedState) -> new MutationResult<>(nonMutatedState, value)
);
}
}
}
Monad則上、doNothingReturning
はMonadの値を作って返すだけで、それ以外のことはしてはいけません。
このルールに加えて、先ほど説明した、「State<S, T1>
は、S
とT1
のmutator、すなわち、書き換えた状態と、一緒に別の値を返す関数をラップしただけのクラスである」という事実を思い出してください。
それでは「書き換えた状態と、一緒に別の値を返す関数」のうち、「何もしない」ものを作るにはどうすればよいでしょう?
「状態を書き換えないで、そのまま返す」というのが正解です。
State.Return.doNothingReturning
は、
(nonMutatedState) -> new MutationResult<>(nonMutatedState, value)
という、
nonMutatedState
を何も書き換えずにそのまま返し、- 加えて引数
value
を「一緒に返す別の値」として返すだけ
の関数を作っているのです。
繰り返しになりますが、State.Return.doNothingReturning
は、「受け取った状態を全く書き換えないで返す関数」を作る、ただそれだけのことをしています。
State.then
でやっていること
それでは本題、Monadインターフェースにとって最も重要なメソッド、State
のthen
の詳細を解説しましょう。
説明のために番号をコメントにふって載せますね。↓
class State<S, T1> implements Monad<T1> {
public final Function<S, MutationResult<T1, S>> mutator;
// ...
<T2> State<S, T2> then(Function<T1, State<S, T2>> nextAction){
// (1) 新しくmutatorを作って
<S, MutationResult<T2, S>> composedMutator = (oldState) -> {
Function// (2) 古い状態を書き換え、結果を受けとる
<T1, S> result = this.mutator.apply(oldState);
MutationResult// (3) (2)で状態を書き換えた際に一緒に返した値を、次のState Monadを作る関数へ渡す。
State<S, T2> other = nextAction.apply(result.value);
// (4) (2)で状態を書き換えた状態をまた処理する
return other.mutator.apply(result.newState);
};
// (1) また別のState Monadとしてくるみなおす。
return new State<>(composedMutator);
}
// ...
}
まずは(1)
をつけた2ヶ所に注目してください。
コメントに書いた通りですが、State.then
メソッドでは新しいmutatorを作って、またState
オブジェクトとしてくるみなおしています。
後半の、State
オブジェクトとしてくるみなおす部分は、型を合わせるためだけのものです。
なのでState
Monadのthen
は(そしてその糖衣構文であるdo
も)、「mutatorを新しく作ることで、いい感じに組み合わせる」ためにある、言う点を覚えておいてください。
(2)
は最初の状態書き換えです。結果をMutationResult
として返すことで「書き換えた後の状態」と「一緒に返す値」を、(3)
や(4)
でそれぞれを利用できるようにします。
(3)
では、(2)
で手に入れた「書き換えた結果(MutationResult
)」のうち、「一緒に返す値」を利用して、新しいState
Monadを作ります。
(4)
では、(3)
で手に入れたState
Monadのmutatorを利用して、(2)
で書き換えた状態(MutationResult.newState
)を更に書き換えます。
そうして(4)
で返されたMutationResult
が、State.then
メソッド1回でできる最終的な状態の書き換えの結果となります。
さて、(1)
~(4)
まで色々やりましたが、結局のところ、State.then
は全体として何をしているのでしょうか。あるいは、mutatorをどのように「いい感じに」組み合わせているのでしょうか?
それは、最初のmutator(this.mutator
)でmutate
してから、引数で渡されたmutator(other.mutator
)でmutate
するように組み合わせている、ただそれだけです。
ややこしいのは途中でMutationResult
を「書き換えた状態」と「一緒に返す値」に分解しているところです。
上記のコードのstate
を処理している箇所のみに注目してみてください。
oldState
をthis.mutator.apply
した結果をother.mutator.apply
しているだけ、ということにお気づきでしょうか?
更に簡潔にまとめますと、State.then
は状態を書き換える関数(mutator)を、続けて実行するよう組み合わせるということをしています。
具体的な使い方
説明が長くなってしまいましたが、いよいよState
Monadの使用例を示しましょう。
ひとまずここまで挙げたメソッドを組み合わせて、実用上ないと困る、ユーティリティメソッドを作ってみます。
class State<S, T1> implements Monad<T1> {
/* ... */
// 引数で与えた状態に書き換える
public static <S> State<S, Void> put(S newState){
<S, MutationResult<Void, S>> putter =
Function(ignoredState) -> new MutationResult<>(newState, null);
return new State<>(putter);
}
// 現在の状態を取得する
public static <S> State<S, S> get() {
<S, MutationResult<S, S>> getter =
Function(currentState) -> new MutationResult<>(currentState, currentState);
return new State<>(getter);
}
// 引数で与えた関数で、状態を書き換える
public static <S> State<S, Void> modify(Function<S, S> simpleMutator){
<S, MutationResult<Void, S>> modifier =
Function(currentState) -> {
= simpleMutator.apply(currentState);
S newState return new MutationResult<>(newState, null);
};
return new State<>(modifier);
}
/* ... */
}
まずはState.put
について。
State.put
は新しい状態(newState
)を引数として受け取り、前の状態(ignoredState
)を使わず、そのまま新しい状態(newState
)で書き換えるmutatorを返します。
new MutationResult<>()
の第一引数が「書き換えた新しい状態」で、第二引数が「一緒に返す値」であることを思い出してください。
State.put
メソッドでは「書き換えた新しい状態」として受け取ったnewState
をそのまま渡し、「一緒に返す値」としてnull
、すなわち「一緒に返す値」がないものとしています。
これは、Javaで例えるなら戻り値がvoid
のメソッドのようなものです。戻り値の型がState<S, Void>
になっている通りです。
続いてState.get
について。
State.get
がラップして返すmutatorは、引数として受けとるcurrentState
をそっくりそのままnew MutationResult<>()
の第一引数として渡して返します。
状態を一切書き換えないのです。あくまでもget
するだけですからね。
そして状態を書き換えた際に「一緒に返す値」であるnew MutationResult<>()
の第二引数にも、currentState
をそのまま渡しています。
これにより、State.get()
を使うと、現在の状態をそのまま取得することができます。
最後にState.modify
について。
State.put
と同様に、State.modify
も戻り値の型はState<S, Void>
です。すなわち状態を書き換えるだけで、「一緒に返す値」を返しません。
State.put
が新しい状態を引数から受け取っていたのに対して、State.modify
では、前の状態に(第一引数として受け取った)関数を適用することで、新しい状態を作ります。
simpleMutator
と呼んでいる通り第一引数の関数は、単純に書き換える前の状態を受け取って、変更後の状態を返す関数なのです。
State
Monadは実用上、ここまで紹介したユーティリティメソッド、State.put
・State.get
・State.modify
を中心に利用した方が、直感的で使いやすいかと思います。
と、いうわけで上記のState.get
とState.put
を利用して、StringBuilder
を真似したような例を紹介しましょう。
State<String, Void> builder =
// 状態を取得して、次の関数に渡す。
State.get()
// ↓getから渡された状態に処理を加え、書き戻す。
.then((currentString) -> State.put(currentString + "one!"))
.then((_null /* どうせ State.put が返す(次の関数に渡す)値はnullなので無視する */) ->
// ↓また状態を取得する。今度は前の行でputした後の状態が返ってくる。
State.get()
)
// あとはその繰り返し。
.then((currentString) -> State.put(currentString + "two!"))
.then((_null) -> State.get())
.then((currentString) -> State.put(currentString + "three!"));
System.out.println(
// 初期値を渡すことで、初期値から書き換えていった結果が得られる
// この場合 "zero! one!two!three!" と出力される
.mutator.apply("zero! ").newState
builder);
あるいは、State.modify
を利用して次のように書くこともできます。
State<String, Void> builder =
State.modify((currentString) -> currentString + "one!")
.then((_null) -> State.modify((currentString) -> currentString + "two!"))
.then((_null) -> State.modify((currentString) -> currentString + "three!"));
今回のケースではこちらの方がすっきりして分かりやすいでしょうね。
何が嬉しいの?
さて、ここまで長々とState
Monadについて説明しましたが、いったいこんなものを作って何が嬉しいのでしょう?
この章の冒頭でも触れた通り正直に言って、Javaではあまり役に立ちません。
特に上記の例に限って言えば、普通にStringBuilder
を使って書いた方が読みやすさの観点からも実行効率の観点からも明らかに優れています。
そもそもJavaでは変数もオブジェクトもミュータブル(変更可能)なのですから、敢えてこんなものを作る必要はないですしね。
一方、これはHaskellでは結構役に立ちます。
Haskellでは、型によって関数のできることに厳格な制約がかけられます(主にデバッグのために使われる例外はあります)。
通常の関数は「決められた型の値を受け取って、決められた型の値を返す」以外に、全く何も出来ません。
Javaではクラスの設計によって、利用者がそのクラスを使ってできることに制限を加えたりできますが、ある意味、Haskellでは言語仕様レベルでそのような厳重な制限がかけられています。
このことは一見厳しすぎるように見えるかも知れませんが、実際には極めて大きなアドバンテージでもあります。
本記事の守備範囲から離れてしまうので、お話しできないのが残念なくらいには。
しかし実際問題として、こうした制約が面倒に感じられることも多々あります。
よくあるのは、値を返しては別の関数に渡し、また値を返しては別の関数に渡し…というのを繰り返したいケースです。
こうしたケースでは、新しい変数名を考えるのもかったるいくらい、冗長な記述になってしまうでしょう。
それから、型を定義して表現したい対象について、命令型プログラミングスタイルで考えた方が相性がよいように感じられる場合、というのもあるでしょうね。
HaskellのState Monadは、そうしたケースにおいて、ちょうどよい「抜け道」を空けてくれます。
先程の例をよく振り返っていただきたいのですが、変数 builder
を作るのに使うラムダ式や、State
Monadの各種メソッドに至るまで、Java組み込みのSystem.out.println
以外、すべて純粋な関数のみで構成されていることにお気づきでしょうか?
builder.mutator.apply("zero! ")
してからnewState
を得るまでの全ての関数が、「戻り値が引数によってのみ定まり、『戻り値を返す』以外になにもしない」「純粋な関数」でできているのです。
このようにState
Monadは、純粋な関数のみを使っているのにあたかも命令型スタイルで書かれているかのように見せる、魔法のようなMonadとなのです。
do記法を使って書き換えると、そのことをもっと実感できるでしょう。
ただし、先程のMaybe
Monadの例から、もう少しdo記法でできることを増やしましょう。
State
Monadの例において、
.then((currentString) -> State.put(currentString + "one!"))
.then((_null /* どうせ State.put が返す(次の関数に渡す)値はnullなので無視する */) ->
// ↓また状態を取得する。
State.get()
)
とか、
.then((_null) -> State.modify((currentString) -> currentString + "three!"));
などのthen
メソッドでは、コメントにも書いた通り、ラムダ式の第1引数(_null
)が無視されています。
これは、State.put
メソッドやState.modify
メソッドが「状態を書き換えた際に、一緒に返す値」としてnull
を返しているため、役立たずなためです。
そうしたケースをより簡単に書けるように、then
メソッドに渡す関数の、第1引数にあたる箇所を省略できるようにしましょう。
その結果がこれ↓です。
State<String, Void> builder = do {
<- State.get();
currentString State.put(currentString + "one!");
<- State.get();
currentString State.put(currentString + "two!");
<- State.get();
currentString State.put(currentString + "three!");
};
State<String, Void> builder = do {
State.modify((currentString) -> currentString + "one!");
State.modify((currentString) -> currentString + "two!");
State.modify((currentString) -> currentString + "three!");
};
ますます普通の手続き型スタイルっぽくなりましたね!
State.put
したりState.modify
することによって書き換えられる新しい状態が、do
ブロックの中で直接現れず、do
ブロック全体で共有されるようになります。
State.get
の結果か、State.modify
に渡したラムダ式の中からでしか、現在の状態は見えません。「現在の状態」はdo
ブロックの中で暗黙に共有されるようになるのです。
さて、この場合どう糖衣構文で簡略化されたのでしょう?
元のコードのthen
メソッドの呼び出しが、セミコロン「;
」、つまり文の終端に置き換わったと考えると分かりやすいです。
State.modify
などが返したState
Monadを、セミコロン 「;
」 がthen
メソッドを呼び出して処理しているのです。具体的には、繰り返しになりますが、「状態を書き換える関数(mutator)を、続けて実行するよう組み合わせて」いるのです。
これはMonadを「プログラマブルセミコロン」として例える主張ともぴったり一致します。
上記の主張を借りるならば、Monadは(+
などの演算子と同様に)セミコロンをオーバーロードできるようにしてくれるのです。
Stateまとめ
長くなってしまいましたのでここで一旦まとめます。もう次が「最後に」なんですけどね。
State
Monadではthen
メソッドが、状態を書き換える関数を続けて実行するよう組み合わせる。State
Monadは純粋な関数だけの組み合わせで、命令型プログラミングのスタイルで書けるようにすることができる。State
Monadは、「関数は全て純粋でないとならない」という(Haskellなどの)制約において、「抜け道」を作るのに役に立つ。- Monadは(
+
などの演算子と同様に)セミコロンをオーバーロードできるようにしてくれる。
最後に
Monadはセミコロン、つまり文と文を繋ぐもの、「各文の間にあるもの」を変えることを可能にします。
Maybe
Monadの例ではそのことに触れず「細い矢印 <-
の箇所に置き換わった」と説明しましたが、「セミコロンの箇所でnull
チェックをしている」と解釈しても今ならそんなに違和感ありませんよね?
また、Monadの説明でよく出てくる「文脈」6という概念はここで言う「各文の間にあるもの」と捉えていただくと、もっといろいろしっくり来るのではないでしょうか。
例えばMaybe
Monadであれば、各文の間に「結果がNothing
でないかどうかチェックし、Nothing
であれば次の文の実行をやめる」という処理がありますし、State Monadであれば、「前の文で書き換えた状態を、次の文に渡す」という処理があります。
他のMonadをご存じであれば、それも考えてみてください。各Monadへの理解がいっそう深まることでしょう。
繰り返しになりますが、最後にまとめをもう一度挙げておきます。
- MonadをJavaで表現すると、インターフェースで表現することができる。
- do記法があるおかげで、HaskellではMonadが思いの外役に立つ。
- do記法をdo記法らしく直感的に使えるようにするためには、Monadインターフェースを実装したクラスが、Monad則というルールを守る必要がある。
- Monadは各文と文を繋ぐものの意味を変える、すなわち、(
+
などの演算子と同様に)セミコロンをオーバーロードできるようにしてくれる。
それから、レビューにご協力いただいたみなさん、特にlotz84さんやitkrt2yさん、お忙しい中ありがとうございました!
突然のお願いにもかかわらず真摯な指摘をいただけました。おかげでもっとNon-Haskeller目線にたった記事が書けたと思います!
それではMonadでHappy Hacking!
参考
- モナドについてSwiftで説明してみた - Qiita
- java - How can I force a Constructor to be defined in all subclass of my abstract class - Stack Overflow
- Javaラムダ式メモ(Hishidama’s Java8 Lambda Expression Memo)
- Monads, or Programmable Semicolons | Zack’s Blog
- 改訂2版 パーフェクトJava
- 関数プログラミング実践入門
念のため補足しますと、ここで説明するMonadはあくまでもプログラミングの世界、特にHaskellで使われているあの「Monad」の話です。あまり触れられませんが、圏論の世界で言う「モナド」とは目的も定義も厳密には異なります(どちらかというと圏論の「モナド」の方が定義が広いです)。アルファベットで「Monad」と表記しているのも、Haskellで使われているあの「Monad」がソースコードの中で「Monad」と表記されているためです。↩︎
Haskellに詳しい方はすぐに察することができるかと思いますが、
Monad
のthen
メソッドがHaskellでいうところの>>=
(bind)に相当し、Monad.Return
のdoNothingReturning
メソッドがreturn
に相当します。↩︎参照型と値型の区別はこの際お見逃しください。やっぱりJavaで厳密にMonadを表現するのは難しい。↩︎
ちなみに、Haskellの
Maybe
とはちょっと異なる振る舞いをします。 HaskellのMaybe
は、return
がnull
に相当するNothing
を返すことはないからです。null
とNothing
は実際には大きく異なるものなので単純に比較できませんが。詳しくはHaskellを勉強してみて確かめましょう!↩︎Javaの関数オブジェクトやラムダ式の仕様上、本記事のMonad則で現れる
(x) -> ax.apply(x)
のような式は、本当は単にax
と書き換えることができます。
それから、(y) -> r.doNothingReturning(y)
も、単にr::doNothingReturning
と書くだけでOKです。
ですが説明しやすさのために敢えて冗長な記述をしております。ご容赦を。↩︎例えば関数プログラミング実践入門の241ページの「本章で説明するモナドは、端的には『文脈を伴う計算』同士を組み合わせ可能にするしくみです」という文。↩︎