Help us understand the problem. What is going on with this article?

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"));
kawasima
Clojure関連のことをブログがわりに書き綴ります。 ※ここでの発言はシステムエンジニアを代表するものであって、所属する組織は二の次です。
https://github.com/kawasima/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした