uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

Java 8のOptionalをGroovyから超簡潔に使用する

結論

Java8のOptionalは超すっきり扱えるよ、そう、Groovyならね。

Optionalって何?

Java 8で導入される新規クラスの一つ、java.util.Optionalは、メソッドの実行結果で成功する場合と失敗する場合があるときに、その返り値で成功と失敗を表現するためのものです。

  • Opitonalは単一要素を保持するコンテナ型。成功した場合は返り値をコンテナで保持させたものを返す。(成功時の返り値をラッピングする)
  • 「失敗」は固定のシングルトン(Optional.empty())として扱う

まあ、それだけの話といえばそれだけなのですが、効果は、

  • 失敗のある可能性のあるメソッドと無いメソッドをコメントではなくプログラムの一部として明示し、両者の違いをコーディング上も区別する
  • 失敗のある可能性のあるメソッドと無い混在・混同することのないようにコンパイル時チェックをできるようにする*1
  • 全体としては、nullで失敗値を表現する場合と比べ、NullPointerExceptionが発生しにくくなる

というところ。しかしながら、ラッピングされた値を取り出すにはOptional.get()を使うのですが、Optiona.empty()に対してget()を実行すると、NoSuchElementExceptionが発生します。なので、getを実行して値を取り出す場合には、Optional.isPresent()で、値がemptyではないことのチェックをする必要があります。でもそれって、nullチェックと同等なのであって、Optionalの目的からして悲しいものがあります。

なので、Optionalに関しては、値を取り出さずに操作を可能とするいくつかのメソッド(orElse,orElseGet,orElseThrow,map,flatMap, ifPresent, )を積極的に使うべきです。

問題は、これらのメソッド群が、単一のOptional値を操作するものでしかないということです。例えばそれぞれString型を保持する2つのOptional値があったとき、それを文字列結合したいとします。(この例はきしださんの記事より)。その場合以下のようにする必要があります。

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

これは駄目な方のパターンですね。そのために、flatMapというものがあり、

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

こんな風に書けます*2。Optionalを使ったコーディングのキモは、「成功か失敗か」の個々値を組み合せた結果も「成功か失敗か」を正しく伝搬していることです。flatMap()はそれに関わってます(「複数のOptionalを組み合せた場合、どれか1つでも失敗したら全体は失敗」という意味を実装しているのはOptinalのflatMap())。
しかし、これらが、もともとやりたかったことは、

String fullName = String.join(" ", lastName, firstName);

だったわけですが、Optionalがちりばめられていて、たとえLambdaであろうとも、ちょっとごたごたしてますね。いやちょっとどころではない。

それ、Groovyで(ry

GroovyはJava 8に正式対応してるか不明ですが、試したbuild 1.8.0-ea-b115というバージョンでは動いているようです。

どうするかというと、Optionalに以下のようなメソッドを追加します。

Optional.metaClass.methodMissing = { String name, Object args ->
    assert args instanceof Object[]
    for (int i=0; i<args.size(); i++) {
        if (args[i] instanceof Optional) {
            if (args[i].isPresent()) {
                args[i] = args[i].get()
            }
            else {
                return Optional.empty()
            }
        }
    }
    if (delegate.isPresent()) {
        return Optional.ofNullable(delegate.get().invokeMethod(name, args))
    }
    else {
        return Optional.empty()
    }
}

ここではExpandoMetaclassを用いてますが、追加方法はuseでも拡張メソッド(ExtensionMethod)を含むモジュールとしてでも構わないでしょう。

すると、

Optional<String> fullName = lastName+" "+firstName

と書けます。つまりOptionalであることを意識せずに、通常の値と同様に透過的に操作でき、結果は結果をOptionalで包んだものになります。また、firstNameとlastNameのいずれかもしくは両方がOptional.empty()であるときには結果もOptional.empty()となります。

試してみます。

        Optional<Integer> firstName = Optional.of("FirstName")
        Optional<Integer> lastName = Optional.of("LastName")
        Optional NULL = Optional.empty()
        assert (lastName + " " + firstName).get() == "LastName FirstName"
        assert (NULL     + " " + firstName) == NULL
        assert (lastName + " " + NULL) == NULL
        assert (NULL     + " " + NULL) == NULL

全体コードはこちら

制約

もっとも制約もあって、Optionalに対する直接的なメソッド呼び出しを置き換えるだけなので、

  • Optional値を、他のメソッド(例えばString.join())などに渡すというケースには介入できない。上記でString.join()を使わずに+" "+しているのはこれが理由。+は左辺に対するplusメソッドの呼び出しとGroovyでは解釈されるので、それをOptionalのそれを置き換えることで実現されるからです。
  • 他にもあるかな?

という制約もあります。x.method(y,z,..)というパターンにおける失敗値の伝搬を解決するだけです。でもだいぶ可読性が高くなると思います。

Optional値を他のメソッド(例えばString.join())などに渡すというケースについては、以下のようにクロージャをOptionalに包んだ上でcallをmissingMethodでトラップしてクロージャに転送してもらえば良いですね。

assert (Optional.of{a,b -> String.join(' ', a, b) }("A", "B")) == Optional.of("A B")

完璧!

結論

Java8もGroovyで!
enjoy!

おまけ

Optionalをアプリカティブにする*3、ってのも考えましたが、結果は表記がGroovyとしてのメソッド呼び出しっぽくないので、いまいち。「&」がHaskellの<*>(ap)で%(mod)が<$>(fmap)に対応させています。全体はこちらに公開しておきます。

Optional<String> arg1 = Optional.of("Abc")
Optional<String> arg2 = Optional.of("Def")
Optional<String> NULL = Optional.empty()

assert ({a,b -> a+' '+b} % arg1 & arg2) == Optional.of("Abc Def")
assert ({a,b -> a+' '+b} % arg1 & NULL) == NULL
assert ({a,b -> a+' '+b} % NULL & arg1) == NULL

assert ({a,b -> String.join(' ', a, b)} % arg1 & arg2) == Optional.of("Abc Def")
assert ({a,b -> String.join(' ', a, b)} % arg1 & NULL) == NULL
assert ({a,b -> String.join(' ', a, c)} % NULL & arg1) == NULL

Closure c1 = {a,b -> a+b}
Closure c2 = {a,b -> a.toUpperCase()+b.toLowerCase()}
Closure c3 = {a,b -> a.length() * b.length()}

assert (c1 % arg1 & arg2) == Optional.of("AbcDef")
assert (c1 % arg1 & NULL) == NULL
assert (c1 % NULL & NULL) == NULL
assert (c2 % arg1 & arg2) == Optional.of("ABCdef")
assert (c2 % NULL & arg2) == NULL
assert (c2 % NULL & NULL) == NULL
assert (c3 % arg1 & arg2) == Optional.of(9)

汎用的といえば汎用的です。クロージャを介してですがString.joinも使えます。Haskellの<$>(fmap)に対応させている積りの%(mod)をClosureのメソッドに追加定義したかったところがExpandoMetaClassの制約により実現できず。できた*4

*1:現存するライブラリがすべてOptionalに対応していないことを考えると、この効果を得られるようになるのは控え目に言っても遠大だと言えるでしょう…。Java8のAPIのどれだけをOptional対応にするか、そして既存のnull返しライブラリを使わなくなるか、にもかかっています

*2:こう書けることにはモナド則とかが関わってるのですが、Optionalなら直感的には自明。

*3:Optionalはモナドであり、モナドはアプリカティブより強いのでこれは必ず可能

*4:[http://jira.codehaus.org/browse/GROOVY-3674]