きしだのはてな このページをアンテナに追加 RSSフィード

2013-05-24(金) Java8でのプログラムの構造を変えるOptional、ただしモナドではない

[][]Java8でのプログラムの構造を変えるOptional、ただしモナドではない 10:15 Java8でのプログラムの構造を変えるOptional、ただしモナドではないを含むブックマーク

※ 5/29 3:23 追記:なんかモナドになったかも。最下部参照


さて、Java8での拡張をいろいろ見てきたわけですが、ではアプリケーションプログラムでFunctionを受け取るメソッドをがんがん定義するかというとそういうことはあまりなく、フレームワーク的な部分で数個定義する感じになると思います。もちろん数個でも効果はでかいのですが。

また、おそらくStreamを受け取ったり返したりするメソッドを定義することは、めったにないのではないかと思います。

Mapでの拡張も、メソッド内部での処理記述がかわる話で、メソッドの引数や戻り値はMapのまま変わりありません。


Javaでのプログラムの構造というのは、メソッドの引数や戻り値の型がなんであるかで決まると言うことができます。その意味では、lambdaやStreamというのは処理の記述は変わるけどプログラムの構造は変わらないとなります。

けれどもOptionalは、おそらく戻り値としてアプリケーションプログラムの中でも頻出することになるのではないかと思います。その意味でOptionalは、Javaのプログラムの構造を変えると言えます。


Optionalの基本的な使い方

Optionalとはなにかというと、nullの可能性がある値をラップして、より安全なプログラムが書けるようにする仕組みです。

Optional<String> name = Optional.of("きしだ");

のように使います。

ここでofメソッドにnullを与えると例外NullPointerExceptionが発生するので、値をもたないOptionalオブジェクトを得るときには次のようにemptyメソッドを使います。

Optional<String> name = Optional.empty();

なので、この時点では、nullによる例外が発生しにくくなるというよりは、nullによる例外をより早く発生させる、フェイルファスト的な安全性といえます。


Optionalから値を取り出すときには、getメソッドが使えます。

System.out.println( name.get());

ただし、emptyオブジェクトに対してgetメソッドを呼び出すと例外java.util.NoSuchElementExceptionが発生します。

そのため、getメソッドを使うときには、isPresentメソッドによるチェックが必須になります。

if(name.isPresent()){
  System.out.println( name.get() );
}

getメソッドを使う限りでは、nullチェックがisPresentチェックに、チェックを忘れたときの例外がNullPointerExceptionからNoSuchElementExceptionに変わっただけということになって、あまりメリットはないと言えます。


そこで、Optionalに対しては、値があるときだけ処理を行うという場合にはifPresentメソッドで処理を行います。

name.ifPresent(s -> {
  System.out.println(s);
});

記述量としてはifでisPresentを判定した場合とそう変わりませんが、記述を強制しやすくなると思います。


値を持たない場合のデフォルト値を使うという場合には、orElseメソッドが使えます。

System.out.println("名前:" + name.orElse("指定なし"));

デフォルト値として使うオブジェクトの生成コストが高い場合には、orElseGetメソッドを使います。

System.out.println(name.orElseGet(() -> 
    ResourceBundle.getBundle("messages").getString("name.default")));

また、値がない場合は例外を発生させるという場合には、orElseThrowメソッドを使います。

System.out.println(name.orElseThrow(() -> new Exception("名前がないよー")));

例外オブジェクトの生成はコストが高いので、最初からSupplier経由になっているのだと思います。


ここでorElseThrowのシグネチャ

<E> T orElseThrow(Supplier<E> sup) throws E

のような感じになっているので、検査例外でも投げることができます。あまり見ないシグネチャなのでおもしろいなと思いました。

実際は、extendsなどが入った型指定になっていますが、省略しています。


Optionalに用意されているメソッドはこれだけですが、nullの可能性がある値が少し扱いやすくなります。

ただ、そもそもOptionalを扱う変数にnullを割り当てることができて、その場合にはifPresentメソッドやorEleseメソッドを使ったとしてもそこでNullPointerExceptionが発生するので、あまり強い仕組みではありません。そこは言語としてOptionalを持っているわけではないJava言語の限界ではないかと思います。

Optional<String> name = null;
name.ifPresent(System.out::println); //ぬるぽ

nullの可能性があるというのをシグネチャとして示せるだけでもありがたいという程度ですが、それでもやはりより安全なコードを書くためには大切な仕組みだと思います。

同様の仕組みは、外部ライブラリで用意することも可能ですが、こうやって標準ライブラリに入ることで、ほかの標準ライブラリでもOptionalが使われるようになっていくと思います。その点では標準になったというのは大きいのではないでしょうか。

JPAやJAXBなどマッピング系のライブラリで使えるようになるとありがたいです。


Optionalはモナドではない

ところでOptionalというと、たとえばScalaのOptionやHaskellのMaybeなど他言語での同様の仕組みはモナドの例としてよく取り上げられています。

でも、JavaのOptionalは残念ながらモナドではありません。

Optionalがモナドであるための条件のひとつとして、次のflatMapメソッドのようなシグネチャで、値を別の値に変換してOptionalでくるんで返すメソッドが必要です。

<U> Optional<U> flatMap(Function<T, Optional<U>> mappingFunc);

このようなメソッドはOptionalクラスにはないので、Optionalはモナドではないということになります。


たとえば、StringがOptionalでくるまれたfirstNameとlastNameというオブジェクトがあって、両方が値をもつ場合だけ両方を連結した文字列をOptionalでくるんだfullNameを作りたいとします。どちらかが値をもたなければemptyになるとします。

その場合、いま予定されているJava8のOptionalでは次のようなコードになります。

Optional<String> fullName = (lastName.isPresent() && firstName.isPresent()) ? 
        Optional.of(String.join(" ", lastName.get(), firstName.get())) : 
        Optional.empty();

こうやって、isPresentで判定してgetで値をとるというのでは、Optionalを使わない場合とあまり変わりません。


もしOptionalクラスに上記のflatMapメソッドがあれば、次のように書けます。

Optional<String> fullName = 
  lastName.flatMap(ln -> 
  firstName.flatMap(fn -> 
    Optional.of(String.join(" ", ln, fn))));

変換した値をOptionalでくるんでくれる、次のようなmapメソッドがあれば、もっと記述がすっきりします。

<U> Optional<U> map(Function<T, U> mappingFunc);

そうすると、次のようになりますね。

Optional<String> fullName = 
  lastName.flatMap(ln -> 
  firstName.map(fn -> 
    String.join(" ", ln, fn)));

どうなっているかというと、Optionalの中ではOptionalを気にせずに処理が記述ができて、それでいて外部からはOptionalで包まれているという風になっていて、Optionalの外と中が分離されたコードになっています。モナドというのは、モナドでくるまれた中の世界ではモナドを気にせずに処理が記述でき、外からみるとモナドでくるまれているという、外と中を分離するための仕組みです。

Optionalの役割を考えると、そのように分離されているほうが都合がよく、そのためScalaHaskellでの同様の仕組みはモナドになっているわけです。

Java8でも、Streamはモナドになっているので、Streamの中の処理はStreamを意識せずに記述でき、外からみると常にStreamにくるまれているという風になっています。


でも、Java8のOptionalはそうなっていないので、Optionalにくるまれた値を処理するときには、一旦Optionalの外に値を持ってくる必要がでてきます。これはちょっと残念です。


Optionalの議論

さて、じゃあOptionalがモナドになっていないことを、だれも気にしてないんでしょうか?

もちろんそんなことはなくて、たとえばMario Fuscoという人が、次のようなブログを書いてJava8でのOptionモナドの実装について記述しています。

No more excuses to use null references in Java 8 | Javalobby


その数日後、次のようにメーリングリストに投稿して、Optionalをモナドにしてほしいと要望を出しています。

OpenJDK Lambda Development - Option in Java 8

それに対して、Javaの設計者のひとりBrian Goetzは「JavaのOptionalの目的はOptionモナドの実装ではない」と答えています。


そして、Brian Goetzは次の投稿で、Optionalの議論にこれ以上時間をとらせないでくれというようなことを言っていますね。

Optional require(s) NonNull

この投稿の中で、Optionalの狭い設計対象は、JDKの内部での利用であると述べています。また、Optionモナドの実装や、Optionモナドが解決しようとしている問題を解決することが目的でないと述べています。

flatmapの言語サポートやパターンマッチングなどもない状況では、Optinalをモナドにしても価値は非常に落ちる、と。


Mario Fuscoは、なぜOptionalをモナドにしないか、納得いってないようですけど。

java.util.Optional in Java 8 - Google グループ


まあ、今後のリリースで、Optionalクラスにぺろっとmapメソッドが実装されていたりすることはなさそうです。

※ 5/29 1:49 追記 なんか先ほどぺろっとmap/flatMapメソッドが実装されたっぽい

http://hg.openjdk.java.net/lambda/lambda/jdk/rev/fde3666e6394