183
165

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

JavaAdvent Calendar 2015

Day 7

JavaのOptionalのモナド的な使い方

Last updated at Posted at 2015-12-07

NullPointerException 嫌いですよね!そんなときに頼りになるのが Java 8 から導入された Optional です。

でも、 null の代わりに Optional を積極的に使うとコードが Optional だらけになり、特に次のような場合に面倒くさいです。

  1. Optional<Integer> a があるとき、 a の値を二乗したい。ただし、 aempty の場合は empty を得たい。
  2. Optional<Double> a があるとき、 a の平方根を計算したい。ただし、 aempty または負の数の場合は empty を得たい。なお、 Math.sqrt を安全にした(負の数を渡すと empty を返す)関数 Optional<Double> safeSqrt(double) があるものとして考えて良い。
  3. Optional<Integer> aOptional<Integer> b があるとき、 a + b を計算したい。ただし、 abempty の場合には empty を得たい。

本投稿では、このような場合の簡潔な書き方について説明します(本投稿は "SwiftのOptional型を極める""モナドについてSwiftで説明してみた" を Java 向けにアレンジしたものです)。

map

1. Optional<Integer> a があるとき、 a の値を二乗したい。ただし、 aempty の場合は empty を得たい。

これを単純にやろうとすると次のようなコードになります。

// ダメな例
Optional<Integer> a = Optional.of(3);
Optional<Integer> result;
if (a.isPresent()) { // empty でなければ
    Integer x = a.get(); // 中の値を取り出し
    result = Optional.of(x * x); // 二乗してから Optional で包み直す
} else {
    result = Optional.empty();
}

たかが整数を二乗するためだけに面倒すぎます。

Optionalmap メソッドを使えば次のように書けます。

a.map(x -> x * x)

この式は次のように働きます。

  • もし aempty でなければ Optional に包まれた中の値を取り出す。
    • 取り出された値を引数として x に渡す。
    • 計算された x * xOptional で包み直して返す。
  • もし aempty なら empty を返す。

このように、 map メソッドを使えばわざわざ Optional に包まれた値を取り出してから計算して包みなおさなくても、「二乗する」という操作を直接記述するだけで事足ります

実際に a が値を持つ場合と empty の場合に実行すると次のようになります。

Optional<Integer> a = Optional.of(3);
Optional<Integer> result = a.map(x -> x * x); // Optional[9]
Optional<Integer> a = Optional.empty();
Optional<Integer> result = a.map(x -> x * x); // Optional.empty

map という名前が Streammap と同じなので気になる人もいるかもしれませんが、 Streammap との関連については後ほど述べます。

flatMap

2. Optional<Double> a があるとき、 a の平方根を計算したい。ただし、 aempty または負の数の場合は empty を得たい。なお、 Math.sqrt を安全にした(負の数を渡すと empty を返す)関数 Optional<Double> safeSqrt(double) があるものとして考えて良い。

この safeSqrt は次のように実装できます1

static Optional<Double> safeSqrt(double x) {
    if (x < 0.0) {
        return Optional.empty();
    }

    return Optional.of(Math.sqrt(x));
}

見ての通り、 safeSqrt は引数 x が負のときに empty を返す他は、ただ Math.sqrt の結果を Optional に包んで返しているだけです。

Optionalとエラー

このように、 Optional はエラーを表すためにも使われます。これまで、 nullreturn することでエラーを表すメソッドがあったように、 emptyreturn することでエラーを表すわけです。 null でエラーを表すとチェックを忘れて NullPointerException を引き起こしがちですが、 Optional であれば empty である可能性が型で表されており、値を利用するためには明示的に取り出さないといけないのでより安全です

例えば、 Stream<T> の最初の要素を返す findFirst メソッド2の戻り値の型は T ではなく Optional<T> です。 Stream に要素が存在すれば最初の要素を( Optional に包んで)返しますが、一つも要素がなければ empty を返します。これは、 Java 8 以前から存在する Deque<E>LinkedList などのスーパータイプ)の peekFirst メソッド3の戻り値が E なのとは対照的です。 peekFirst メソッドは要素がなければ null を返すので忘れずにチェックしなければなりません。

検査例外でも安全にエラー処理ができますが、 try, catch は書くのが面倒なのと、式ではなく文になってしまうため計算の途中に組み込むことができません。また、 Optional であればエラーという状態を値のまま扱うことができるため、それをフィールドに格納しておいて後から取り出すなど、 エラーを値として取り回して任意のタイミングで(しかし忘れずに)エラー処理を行うことができます4

フラットにする

さて、本題に戻りましょう。

一見、この場合も map を使えば書けそうに思えます。

a.map(x -> safeSqrt(x))

しかし、これではダメです。上記の式の結果の型は何になるでしょうか?

答えは、 Optional<Double> ではなく Optional<Optional<Double>> です。なぜなら、 mapsafeSqrt(x) の戻り値をさらに Optional で包んで返してしまうので、二重に Optional に包まれてしまうわけです。

この場合、 anull のときには外側の Optionalempty に、 a が負の数の場合には内側の Optionalempty になります。今やりたいことは、それらをフラットに潰して Optional<Double> にし、外と内のどちらか一方でも empty であれば empty にしてしまうことです。

これをやってくれるのが flatMap です。 flatMapmap とフラットに潰す処理をまとめてやってくれるメソッドです。 Optional に包まれた値に適用したい処理がエラーになる可能性があるなど( x * x は失敗しないが safeSqrt(x) は失敗し得る)、 Optional に渡す処理の戻り値も Optional になってしまう場合には flatMap が使えます。

a.flatMap(x -> safeSqrt(x))

概念的には、これは map をしてから二重になった Optional を平らにする( flatten )という処理です。

// ※ このコードは概念を表したものであり、正しい Java のコードではありません。
a.map(x -> safeSqrt(x)).flatten() // map すると二重になったのでフラットにする

なお、 Java には flatten はないですが(というか Optional のインスタンスメソッドとしては Java の構文の限界で適切に宣言できない)、 flatten 相当の処理は次のようにして実現できます。

Optional<Optional<Integer>> nested = Optional.of(Optional.of(42));
Optional<Integer> flattened = nested.flatMap(x -> x); // Optional[42]

a が正の場合、負の場合、 empty の場合に a.flatMap(x -> safeSqrt(x)) を実行すると、次のように望んだ結果が得られます。

Optional<Double> a = Optional.of(2.0);
Optional<Double> result = a.flatMap(x -> safeSqrt(x)); // Optional[1.4142135623730951]
Optional<Double> a = Optional.of(-2.0);
Optional<Double> result = a.flatMap(x -> safeSqrt(x)); // Optional.empty
Optional<Double> a = Optional.empty();
Optional<Double> result = a.flatMap(x -> safeSqrt(x)); // Optional.empty

複数のOptional

3. Optional<Integer> aOptional<Integer> b があるとき、 a + b を計算したい。ただし、 abempty の場合には empty を得たい。

これを愚直にやると次のようになります。

Optional<Integer> a = Optional.of(2);
Optional<Integer> b = Optional.of(3);
Optional<Integer> result;
if (a.isPresent()) {
    if (b.isPresent()) {
        result = Optional.of(a.get() + b.get());
    } else {
        result = Optional.empty();
    }
} else {
    result = Optional.empty();
}

足し算をするためだけにこれは耐えられません。

このようなケースにも、 flatMapmap を組み合わせて使うことで対応できます。

a.flatMap(x -> b.map(y -> x + y))

イメージとしては、 flatMapmap をネストすることで Optional に包まれていない世界をつくり( Optional<Integer> aOptional<Integer> bInteger xInteger y として扱えるようにし)、 x + y を計算するという感じです。計算結果は Optional に包まれますが、外側が flatMap なので二重に包まれてしまうことはありません。

実行結果は次の通りです。 ab の片方でも empty なら結果は empty となります。

Optional<Integer> a = Optional.of(2);
Optional<Integer> b = Optional.of(3);
Optional<Integer> result = a.flatMap(x -> b.map(y -> x + y)); // Optional[5]
Optional<Integer> a = Optional.empty();
Optional<Integer> b = Optional.of(3);
Optional<Integer> result = a.flatMap(x -> b.map(y -> x + y)); // Optional.empty

処理が失敗しうる場合

x + y という失敗しない処理なので、上記のコードでは内側が map になっていますが、失敗しうる処理であれば flatMap を入れ子にする必要があります。

例えば、 safeSqrt と同じように安全な Optional<Double> safePow(double x, double y) を考えます5。その場合、 Optional<Double> aOptional<Double> b に対して safePow を計算するには次のようにします。

a.flatMap(x -> b.flatMap(y -> safePow(x, y)))

三つ以上のOptional

Optional の値が三つ以上になってもその分ネストするだけで対処できます。

例えば、 Optional<Integer> a, Optional<Integer> b, Optional<Integer> c の和を求める計算は次のように書けます。

a.flatMap(x -> b.flatMap(y -> c.map(z -> x + y + z)))

もちろん、処理の結果が Optional になる場合には一番内側の mapflatMap にしないといけません。

コレクションとしてのOptional

Optional は要素が 0 個か 1 個の場合しかないコレクションと考えることもできます。

このことは次の二つのコードを見比べればよくわかります。前者は Optional で書いたもの、後者は同じことを Stream で書いたものです。

Optional<Integer> a = Optional.of(3);
Optional<Integer> b = a.map(x -> x * x); // Optional[9]

Optional<Integer> c = Optional.empty();
Optional<Integer> d = c.map(x -> x * x); // Optional.empty

Optional<Integer> e = Optional.of(2);
Optional<Integer> f = Optional.of(3);
Optional<Integer> sum = e.flatMap(x -> f.map(y -> x + y)); // Optional[5]
Stream<Integer> a = Stream.of(3);
Stream<Integer> b = a.map(x -> x * x); // [9]

Stream<Integer> c = Stream.of();
Stream<Integer> d = c.map(x -> x * x); // []

Stream<Integer> e = Stream.of(2);
Stream<Integer> f = Stream.of(3);
Stream<Integer> sum = e.flatMap(x -> f.map(y -> x + y)); // [5]

そっくりですね!

厳密には Stream はストリームとして一度だけ値を取り出すことしかできずコレクションではないですが、イメージとしては↑で伝わると思います。

filter

Stream には filter というメソッドがあります。 filter を使えば、条件にマッチする要素だけを取り出すことが簡単にできます。

例えば、 Stream<Integer> から偶数の要素だけを取り出すには、 filterx -> x % 2 == 0 ( 2 で割った余りが 0 )を渡します。

Stream<Integer> a = Arrays.asList(1, 2, 3, 4, 5).stream(); // [1, 2, 3, 4, 5]
Stream<Integer> result = a.filter(x -> x % 2 == 0); // [2, 4]

Optionalfilter メソッドを持っています。これは、 Optional を要素が 1 個 か 0 個のコレクションと考えて自然な挙動をします。

例えば、 [2] (一つの要素 2 だけを持つコレクションを意図)に対して先程の filter を実行すると 2 は偶数なので [2] が返されますが、 [3] だと [] (空のコレクション)になってしまいます。また、 [] に対して偶数かどうかで filter しても、最初から要素がないので当然結果は [] です。

Optionalfilter は、このように考えたときと同じように動きます。

Optional<Integer> a = Optional.of(2); // [2] に相当
Optional<Integer> result = a.filter(x -> x % 2 == 0); // Optional[2]
Optional<Integer> a = Optional.of(3); // [3] に相当
Optional<Integer> result = a.filter(x -> x % 2 == 0); // Optional.empty
Optional<Integer> a = Optional.empty(); // [] に相当
Optional<Integer> result = a.filter(x -> x % 2 == 0); // Optional.empty

ループ

コレクションと言えばループです。 Java 8 からは 拡張 for 文 だけでなく forEach
メソッドが使えます。

例えば、次のようにして List の要素を表示することができます。

List<Integer> a = Arrays.asList(2, 3, 5, 7, 11);
a.forEach(x -> System.out.println(x));

実行結果は次の通りです。

2
3
5
7
11

なお、 println するだけであれば次のように forEach メソッドに直接 println を渡すこともできます。

a.forEach(System.out::println);

OptionalforEach メソッドはないですが、同じことをしてくれるのが ifPresent メソッドです。

ifPresent メソッドは、 Optional が値を持っている場合のみ、引数に渡した処理を実行するメソッドですが、 Optional をコレクションだと考えるとこれは forEach に当たります。

次のコードを実行すると 2 が表示されます。

Optional<Integer> a = Optional.of(2); // [2] に相当
a.ifPresent(System.out::println);

しかし、次のコードでは要素がないコレクションの forEach と等価なため、何も表示されません。

Optional<Integer> a = Optional.empty(); // [] に相当
a.ifPresent(System.out::println);

残念ながら OptionalIterable を実装していないので、 拡張 for 文 で使うことはできません。

ifPresentとisPresent+get

ifPresent を使うのと、 isPresent で分岐してから get で値を取り出すのでは似たようなことができます。

しかし、 isPresent + get は、本当に isPresent でチェックした上で get を呼んでいるかコンパイル時にチェックすることができません。もし empty に対して get を呼び出すと実行時エラー( NoSuchElementException )になってしまいます。 ifPresent であればそのような心配は必要ないので、 isPresent + get よりも ifPresent を使うことをオススメします

モナドとしてのOptional

本投稿のタイトルは "JavaのOptionalのモナド的な使い方" ですが、 モナド とは何でしょうか?

実は mapflatMap を使うというのが「モナド的な使い方」の意味するところです。前節で OptionalStream の類似性を見てもらいました。 OptionalStreammapflatMap が同じ意味を持つことがわかったと思います。その共通性がモナドの持つ性質です。 モナドとは、 mapflatMap を持つ存在を抽象化したものです

しかし、 mapflatMap を持っていたら何でもモナドと呼べるわけではありません。いきなり return null するとかいう実装だと、とても「共通」の性質を持つわけがないですよね。モナドであるためには、 モナド則 と呼ばれるルールを満たさなければなりません。

Optional を使う上では知らなくても特に問題はないですが、せっかくなので モナドとは何か についてももう少し正確な説明をします。モナドは関数型言語でよく用いられる概念です。モナドについて調べると大抵 モナド則 が出てきますが、肝心の記述が関数型言語で書かれていてわからん!となることが多いんじゃないかと思います。本節では、モナド則についても Java の構文で記述します。ただし、ややこしいだけなので ? extends のようなワイルドカードは省略します。

モナドとは

Foo<T> が次の条件を満たすとき、 Foo<T> はモナドです。

  • Optionalof のように、単一の T 型の値を包んで Foo<T> を返す静的メソッドまたはコンストラクタを持つ。
  • <U> Foo<U> flatMap(Function<T, Foo<U>>) というメソッドを持つ。
  • flatMap の実装が モナド則(後述)を満たす。
  • Foo<T>ファンクター(後述)である。

ざっくりと言えば、適切に実装された(モナド則を満たした) flatMap を持った型はモナドと言えるでしょう(モナドはファンクターでもあるので map も持っている必要がありますが)。

モナド則

  • Foo.of(x).flatMap(x -> f(x)) == f(x) が成り立つ。
  • foo.flatMap(x -> Foo.of(x)) == foo が成り立つ。
  • foo.flatMap(x -> f(x).flatMap(y -> g(y))) == foo.flatMap(x -> f(x)).flatMap(y -> g(y)) が成り立つ。

モナド則は、中身をじっくり見てみればわかりますが、ごく当たり前に満たすべき性質を言ってるだけです。言ってみれば、積の結合法則 (a * b) * c == a * (b * c) のモナド版のようなものです。
例えば、最初の法則は xFoo で包んでからやっぱり中身の x を取り出してメソッド f を適用した結果は、直接 fx に適用した結果と等しいという意味です。要するに、 of で値を包むときや flatMap で取り出すときに余計なことをするなというだけです。

積の結合法則を満たさなければ数とは言えないというのと同じように、モナド則を満たさなければモナドとは言えないという当然の性質を述べているに過ぎません。自作の Number クラスに結合法則が成り立たないような( * の代わりの) multiply メソッドを実装することはできますがそれが意味を持たないのと同じように、モナド則を満たさないモナドっぽい型を実装することはできますがそれはナンセンスなのです。

他の二つについても、中身をじっくり見てみればそれが何を意味するのかわかると思いますし、当然満たすべきものであることがわかると思います。

ファンクターとは

前述の通り、モナドはファンクターでないといけません。ファンクターとは何でしょうか。

Foo<T> が次の条件を満たすとき、 Foo<T> はファンクターです。

  • <U> Foo<U> map(Function<T, U>) というメソッドを持つ。
  • 上記の実装が ファンクター則(後述)を満たす。

同じくざっくりと言えば、適切に実装された map を持った型はファンクターと言えるでしょう。

ファンクター則

次のような、引数で受けた値を返すだけのメソッド id があるとします。

static <T> id(T x) {
    return x;
}

そのとき、ファンクター則は次のように書けます。

  • 恒等メソッド id (後述)に対して、 foo.map(x -> id(x)) == id(foo) が成り立つ。
  • foo.map(x -> g(f(x))) == foo.map(x -> f(x)).map(y -> g(y)) が成り立つ。

ファンクター則もモナド則同様、じっくり見れば当然満たすべきものだとわかると思います。

まとめ

Optional は憎き NullPointerException と戦うための強力な武器です。でも、 Optional に包まれた値を扱おうとすると少し面倒なことがあります。

そんなときには、 mapflatMap のようなモナド的なメソッドを使えばコードを簡潔に書けることがあります。 mapflatMap を習得して快適な Optional ライフを送りましょう!

おまけ:Optionalと変性

去年の Advent Calendar では Java のジェネリクスと変性について書きました

Optional は大抵の場合、共変として問題ないので、(特にメソッドの引数や戻り値にする場合には) Optional<? extends Foo> の形で使うのがいいと思います。

// 非変の場合( Cat は Animal のサブクラスとする)
Optional<Cat> cat = Optional.of(new Cat());
Optional<Animal> animal = cat; // コンパイルエラー
// 共変の場合( Cat は Animal のサブクラスとする)
Optional<? extends Cat> cat = Optional.of(new Cat());
Optional<? extends Animal> animal = cat; // OK

  1. NaN のことを考えると !(x >= 0.0) の方が適切です。

  2. Javadoc はこちら

  3. Javadoc はこちら

  4. 例外と比較した場合の Optional の欠点はエラーの理由を取得できないことです。 Optional がよく使われる関数型言語などでは、そういう場合は Either<L, R> のような LR のどちらかを保持することができる型を使って、値かエラーかを保持することが行われます。残念ながら Java 8 時点では標準ライブラリで Either 相当のクラスは提供されていません。

  5. 例えば、 $-1.0^{0.5}$ などは実数の範囲で解を持ちません。

183
165
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
183
165

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?