読者です 読者をやめる 読者になる 読者になる

きくらげ観察日記

好きなことを、適当に。

JavaにOptional<T>は必要だったのか

Java

大学の授業で初めてJavaに触れて思ったことです。
Javaに関しては全くの初心者なので、見当違いのことを言ってたらごめんなさい。

Functional Interface

Java 8以降にはFunctional Interface(訳語は関数型インターフェース、かと思いきやコンパイラ内のメッセージでは機能インターフェースとか呼ばれていたりするのでよく分からない)という機能が追加されています。これは抽象メソッドが1つしか無いようなインターフェースの実装をラムダ式っぽく表記できるような機能です。例えば、

class Event {
    ...
}

interface Handler {
    public void handle(Event e);
}

というようなクラスがあったとして、今まではその実装となる無名クラスのインスタンス

Handler h = new Handler() {
        @Override
        public void handle(Event e) {
            ...
        }
    };

というように定義していたのに対して、Java 8からは

Handler h = e -> {
    ...
};

というように略記できる、という新たな文法です。

Optional

これも関数型インターフェースの実装に伴って、Java 8で新たに追加された型です。HaskellでいうMaybe, ScalaでいうOptionみたいなアレですね。
要は値がnullableなのかどうかを型レベルで指定してやりましょう、という趣旨の型ということです。
今まで、

String name = ...;
if (name == null) {
    System.out.println("Anonymous");
} else {
    System.out.println(name);
}

というように書いていたコードを、Optionalを使って書くと

Optional<String> name = ...;
System.out.println(name.orElse("Anonymous"));

と書けるようになります。

最大のメリットは、値が無い場合についての処理を強制できる点でしょうか。
nullを使う場合、「この変数の値がnullを取り得るのか」「この関数はnullを返しうるのか」といった情報は、ドキュメントにのみ存在していました。Optionalを使うことで、それを明示化することができるわけです。
また、nullはほとんど全ての動作に対してNullPointerExceptionを投げますが、かと言ってどのような状況でも常にNullPointerExceptionが投げられるわけではないため、例えば上のコードを間違って以下のように書いてしまった場合、catch節以下はどのような場合でも実行されず、name == nullの場合は単に「null」と出力されます。

String name = ...;
try {
    System.out.println(name);
} catch (NullPointerException e) {
    System.out.println("Anonymous");
}

このような問題を解決するために関数型言語から輸入されてきたのがOptional、というわけですね

JavaにとってのOptionalの立ち位置

確かにOptionalを使うことで「値が無いかもしれない」ような値に対する処理をより簡単に行うことができるようになりました。
しかし、次のような場合はどうでしょう:

Optional<T> maybeValue = ...;
maybeValue.map(value -> {
        SomeClass.someMethodThatMayFail(value);
    });

someMethodThatMayFailは検査例外SomeExceptionを投げるとします。

このようなコードを実際に書くことはできません。Optional.mapの引数はFunctionですが、Functional Interface Functionは以下のような定義になっています(実際のコードではありませんが、ほぼ同じもの)。

interface Function<T, U> {
    public U apply(T t);

    // ...(残りはdefaultメソッド)
}

見ての通り、Function.applyは例外を投げることができません。
コードの全ての部分が純粋な「関数型的」なコードなのであればあまり問題はありませんが、そもそも扱っている言語がJavaなのでそうはいきません。既存のJavaで書かれた資産のほとんどは、何らかのエラーが発生すれば素直に例外を投げます。

解決策としては以下のようなものが考えられますが、どれも一長一短。

ifPresentの中のラムダ式で例外を全てcatchしてしまう

とりあえずまず最初に思い付くであろう方法がこれです。しかし、これができるのは発生した例外がifPresentの中のラムダ式で対応できる場合に限られます。

自前でEitherのような型を作り、例外も含めて値として返す。

根っからの関数型ユーザーであればまず最初に思い付くのはこの方法。残念ながらJavaには現在のところEither型は用意されてませんが、自前で実装して以下のように書くこともできます。

maybeValue.map(value -> {
        try {
            SomeObject hoge = SomeClass.someMethodThatMayFail(value);
            return Either.right(hoge);
        } catch (SomeException e) {
            return Either.left(e);
        }
    });

ただし、この方法を使うとEitherから例外を取り出してthrowし直そうとか考え出した時に再び同じ問題に直面します。

非検査例外に変換して投げる

非検査例外は補足する必要がないため、Function.applyの中でも投げることができます。元の例外をそのままラップした非検査例外を投げて、外でcatchしてから元の検査例外に戻してthrowし直すという方法も考えられます。
……しかし、少しトリッキーですし、この方法で綺麗にコードが書けるとは思えません。特にラムダ式の中で互いに親子関係にない複数の例外を投げる場合、うまく作らないと非検査例外に変換する段階で例外の型情報が失われてしまいます。この方法はやめておいたほうがいいでしょう。

mapの中で例外を投げられるような自前Optionalを定義する
interface FunctionThatMayFail<T, R, E extends Throwable> {
    public R apply(T arg) throws E;
}

例えば以下のようなインターフェースを定義して、自前のOptionalのmapメソッドの引数にFunctionThatMayFailを取るようにすることもできます。しかし、この方法も1つ上の方法と似たような問題を抱えています。



以上のことから考えて、どの場合でも確実にうまくやれる方法はどうも無さそうです。
え? Haskellならうまいこといくのかって? MaybeT (Either e) aを使えば済む話です。
なぜHaskellならうまくいくのかというと、Haskellの場合は検査例外(的な情報)も戻り値の型に含むことができるからです。
Haskellは他の言語に比べて非常に機能が制限されている(例外が投げられなかったり、破壊的代入ができなかったり)かわりに、型に関する機能だけはやたら豊富にあります。それを利用して、Haskellであれば例えば「このコードは何らかのエラーを発生しうる」だとか「この部分では破壊的代入(的な動作)が起こりうる」だとかに相当する、いわばコードのメタ情報のようなものを型に含めることができるのです。そして、それらのメタ情報をモナド変換子を使ってうまく組み合わせることができます。

Javaの場合、そういったメタ情報を柔軟に表わせるほど強力な型機能が無いかわりに、検査例外という全く別な方法を使って発生しうる例外の補足を行っています。
それに対して、Optionalは「型でコードのメタ情報を表す」に近いことを行っています。両者は似ていますが、方法としては全く別なものなので、相互に変換することができません。そのため、両者を同時に扱おうとすると面倒なことになってしまうわけです。

ではどうするべきだったか

そもそも通常の型がデフォルトでnullを取れるような設計が間違いだ。と言ってしまえばそれまでですが、そういう仕様になってしまったものはしょうがない。Java後方互換性を最重要視しているので、今さら仕様変更することもできないでしょうし、今さらするくらいなら別な言語作ってろって話です。
うまいこと状況に合わせて「if (hoge == null)」したり「if (hoge.isPresent())」したりEitherでまとめて返したりを使いわけるしかないんじゃないですかねえ。



まあ、Optionalが便利なことは事実です。僕はこれからもJavaでコード書く機会があればOptional使いまくりますけどね。