Clojure
optional
java8

OptionalとJavaにおけるsomeスレッディングマクロ的なもの

More than 3 years have passed since last update.

Java8で導入されたOptionalは、その使い方に関していまだベストプラクティスがないように見受けられます。


Optional#mapとsomeスレッディングマクロ

私はJavaが言語としてnullを許容しているので、Javaにおいては、頑張ってnullを無くそうとするのではなく、いかにしてnullとうまく付き合っていくかが重要だと考えます。

同様にClojureのプログラムでも、nilが頻繁に出てくるのですが、NullPointerExceptionはあまり発生しません。これは、Clojureネイティブな関数たちにおいては、オブジェクト指向ではないので、nilに対するメソッド呼び出しが発生しないことが大きな理由にもありますが、Javaとの相互運用する部分においては、そうもいきません。

そういう場所ではClojureにおいては、Someスレッディングマクロというものを使います。

user=> (some-> (System/getenv) (.get "HOME") (.substring 1) .toUpperCase)

"HOME/KAWASIMA"

user=> (some-> (System/getenv) (.get "HOGE") (.substring 1) .toUpperCase)
nil

結果がnilになると、そこで処理をやめてnilを返すという極めてシンプルなものです。が、このsomeスレッディングマクロは、Javaオブジェクトのメソッドを、ガンガン呼んでもNullPointerExceptionの発生を避けることができる非常に実用的なマクロといえます。

似たことはやはりOptionalを使って可能です。

Optional.of(System.getenv())

.map(env -> env.get("HOGE"))
.map(hoge -> hoge.substring(1))
.map(String::toUpperCase)

someスレッディングマクロ好きな私としては、このためにOptionalがあると言っても過言ではないと思います。


ラムダ式における検査例外の面倒さ

よくあるケースとして、String型のファイルパスを受け取って、URLにして返すみたいなメソッドをつくる場合、Optional#mapを使うと以下のようになるかと思います。

Optional<URL> url = Optional.ofNullable(path)

.map(File::new)
.map(File::toURI)
.map(uri -> {
try {
return uri.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
});

ここでイヤな感じなのは、ofNullableMalormedURLExceptionをハンドリングするために、try-catchを書かなきゃいけないところです。


Someスレッディングマクロ的なものを実装する

なので、ここは一丁もう少し使いやすいAPIを実装してみます。

public class ThreadingUtils {

private static <X, Y> Optional<Y> doSome(X start, ThreadingFunction... functions) {
if (functions == null || start == null) {
return Optional.ofNullable((Y) start);
}

Object v = start;
LinkedList<ThreadingFunction> funcQueue = new LinkedList<>(Arrays.asList(functions));
while(!funcQueue.isEmpty()) {
ThreadingFunction f = funcQueue.removeFirst();
try {
v = f.apply(v);
} catch (Exception e) {
throw new RuntimeException(e); // FIXME もうちょっとちゃんとハンドリングする
}
if (v == null) {
return Optional.empty();
}
}
return Optional.of((Y) v);
}

public static <X, Y> Optional<Y> some(X start, ThreadingFunction<X, Y> f1) {
return doSome(start, new ThreadingFunction[]{ f1 });
}

public static <X0, X1, Y> Optional<Y> some(X0 start, ThreadingFunction<X0, X1> f1, ThreadingFunction<X1, Y> f2) {
return doSome(start, new ThreadingFunction[]{ f1, f2 });
}

public static <X0, X1, X2, Y> Optional<Y> some(X0 start,
ThreadingFunction<X0, X1> f1,
ThreadingFunction<X1, X2> f2,
ThreadingFunction<X2, Y> f3) {
return doSome(start, new ThreadingFunction[]{ f1, f2, f3 });
}

public static <X0, X1, X2, X3, Y> Optional<Y> some(
X0 start,
ThreadingFunction<X0, X1> f1,
ThreadingFunction<X1, X2> f2,
ThreadingFunction<X2, X3> f3,
ThreadingFunction<X2, Y> f4) {
return doSome(start, new ThreadingFunction[]{ f1, f2, f3, f4});
}

public static <X, X1, Y> ThreadingFunction<X, Y> partial(ThreadingBiFunction<X, X1, Y> f, X1 arg) {
return x -> f.apply(x, arg);
}
}

ThreadingFunctionは以下のような、Functionにthrows節を付け加えただけのものです。

public interface ThreadingFunction<T, R> {

R apply(T t) throws Exception;
}

最後にOptional化して返し、途中で評価結果がnullになったらそこで打ち切って、Optional.empty()が返ります。これで先のSting型のパスからURLオブジェクトに変換する処理は、以下のようにスッキリかけます。

Optional<URL> url = some(path, File::new, File::toURI, URI::toURL);

ここまでくると、メソッド参照をもっと活用したくなります。引数を2つ以上とる場合も、部分適用を使えば、以下のとおりメソッド参照で書けるようになります。

Optional<String> encoded = some(str, partial(URLEncoder::encode, "UTF-8"));