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

JavaでList<Optional<T>>をOptional<List<T>>にする

TL;DR

『Scala関数型デザイン&プログラミング』に出てくるsequence関数とtraverse関数のJava版
Scala関数型デザイン&プログラミング―Scalazコントリビューターによる関数型徹底ガイド

やりたいこと(の内の1つ)

Optionalのリストを受け取って、すべて値がある(= isPresentがtrue)ならOptionalを外した値のリストにしたい。
もし1つでも値がnull(= isPresentがfalse)なら、Optional.emptyを返したい。

実装

ざっくりと、こんな感じに書けると思います。
引数またはリストの要素にnullが入っていた場合はエラーになりますが、ここでは考慮しません。(nullを使うほうが悪い)

public static <T> Optional<List<T>> sequence(List<Optional<T>> xs) {
    List<T> ys = new ArrayList<>();
    for (Optional<T> x : xs) {
        // 値が存在しない要素があったらその時点で処理を打ち切る
        if(x.isEmpty()) {
            return Optional.empty();
        }

        // 値が存在すればOptionalのラップを外してリストに追加
        ys.add(x.get());
    }

    // 値だけになったリストをOptionalでラップして返す
    return Optional.of(ys);
}

使ってみる

List<Optional<String>>  xs = List.of(Optional.of("foo"), Optional.of("bar"));
List<Optional<Integer>> ys = List.of(Optional.of(1), Optional.empty(), Optional.of(100));
List<Optional<Boolean>> zs = List.of(); // 空のリスト

System.out.println(sequence(xs)); // Optional[[foo, bar]]
System.out.println(sequence(ys)); // Optional.empty
System.out.println(sequence(zs)); // Optional[[]]

ちゃんと動いてるようです。

応用

文字列を数値に変換する、以下の関数があるとします。
この関数は対象文字列が数値に変換できれば、その数値をOptionalに包んで返します。
もし変換に失敗した場合はOptional.emptyを返します。1

public static Optional<Integer> safeParseInt(String s) {
    try {
        Integer n = Integer.valueOf(s);
        return Optional.of(n);
    } catch (NumberFormatException e) {
        return Optional.empty();
    }
}

この関数を使用し、StringのリストをOptional<Integer>のリストに変換し、さらにsequence関数を適用したときのように、要素にOptional.emptyが含まれる場合は全体の結果もOptional.emptyにする関数traverseを考えます。

// これはすべて数値に変換可能なのでOptional[[1, 100, 30]]にしたい
Optional<List<Integer>> xs = traverse(List.of("1", "100", "30"));

// これは数値に変換できない値が混じっているのでOptional.emptyにしたい
Optional<List<Integer>> ys = traverse(List.of("1", "3", "hoge", "0", ""));

これは、先のsequence関数を使って、次のように実装できそうです。

public static Optional<List<Integer>> traverse(List<String> xs) {
    // 一旦Optional<Integer>のリストに変換
    List<Optional<Integer>> ys = new ArrayList<>();
    for (String x : xs) {
        Optional<Integer> o = safeParseInt(x);
        ys.add(o);
    }

    // sequence関数でList<Optional<Integer>>をOptional<List<Integer>>に変換
    return sequence(ys);
}

動かしてみると、望んだ結果が得られていることがわかります。

System.out.println(traverse(List.of("1", "100", "30")));          // Optional[[1, 100, 30]]
System.out.println(traverse(List.of("1", "3", "hoge", "0", ""))); // Optional.empty

しかし、上記は少しもったいない書き方をしています。
次の例を見てください。travase関数をインライン化し、途中結果も踏まえて出力しています。

List<String> xs = List.of("1", "hoge", "2", "3", "4", "5", "6", "7", "8", "9");

// ここはtravarse関数と全く同じ処理
List<Optional<Integer>> ys = new ArrayList<>();
for (String x : xs) {
    Optional<Integer> o = safeParseInt(x);
    ys.add(o);
}
Optional<List<Integer>> zs = sequence(ys);

// 中間リストと結果を出力
System.out.println(ys); // [Optional[1], Optional.empty, Optional[2], Optional[3], Optional[4], Optional[5], Optional[6], Optional[7], Optional[8], Optional[9]]
System.out.println(zs); // Optional.empty

zsOptional.emptyになること自体は想定どおりです。
しかし、そのためにxsと同じ長さの中間リストysが生成されてしまっています。
xsの2番目の要素"hoge"にsafeParseInt関数を適用し、Optional.emptyが出来上がった時点で、全体の結果もOptional.emptyとなることは決定事項です。
結果がわかっているのにその後も変換処理を続けるのは、少し効率が悪いように感じます。
要素数が少なければ問題になることはあまりないと思いますが、要素数が多い場合はパフォーマンスに影響が出る可能性も考えられます。
Optional.emptyになった時点でそれ以降の要素にはsafeParseInt関数を適用せず、ただちにOptional.emptyが返ってくるようにしたいところです。

そうなるように、travarse関数を書き換えます。

public static Optional<List<Integer>> traverse(List<String> xs) {
    List<Integer> ys = new ArrayList<>();
    for (String x : xs) {
        // 文字列を数値に変換
        Optional<Integer> o = safeParseInt(x);

        // 変換に失敗したらその時点で処理を打ち切る
        if(o.isEmpty()) {
            return Optional.empty();
        }

        // 変換に成功すればOptionalのラップを外してリストに追加
        ys.add(o.get());           
    }

    // 数値だけになったリストをOptionalでラップして返す
    return Optional.of(ys);
}

sequence関数を使うのをやめ、一回のループ内で変換処理と分岐の両方を行っています。
これで、途中で変換に失敗したらその時点でただちにOptional.emptyが返ってくるようになりました。
safeParseIntをパラメータにして、Optionalに変換できる関数なら何でも使えるようにします。

public static <T, R> Optional<List<R>> traverse(List<T> xs, Function<T, Optional<R>> f) {
    List<R> ys = new ArrayList<>();
    for (T x : xs) {
        Optional<R> o = f.apply(x);
        if(o.isEmpty()) {
            return Optional.empty();
        }
        ys.add(o.get());
    }

    return Optional.of(ys);
}

使うときは、ラムダ式またはメソッド参照で渡します。
まとめると、次のようになります。

Main.java
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        // ラムダ式
        Optional<List<Integer>> xs = traverse(List.of("1", "100", "30"), x -> safeParseInt(x));
        // メソッド参照
        Optional<List<Integer>> ys = traverse(List.of("1", "3", "hoge", "0"), Main::safeParseInt);

        System.out.println(xs); // Optional[[1, 100, 30]]
        System.out.println(ys); // Optional.empty
    }

    public static Optional<Integer> safeParseInt(String s) {
        try {
            Integer n = Integer.valueOf(s);
            return Optional.of(n);
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }

    public static <T, R> Optional<List<R>> traverse(List<T> xs, Function<T, Optional<R>> f) {
        List<R> ys = new ArrayList<>();
        for (T x : xs) {
            Optional<R> o = f.apply(x);
            if(o.isEmpty()) {
                return Optional.empty();
            }
            ys.add(o.get());
        }
        return Optional.of(ys);
    }

// 使わなくなった
//    public static <T> Optional<List<T>> sequence(List<Optional<T>> xs) {
//        List<T> ys = new ArrayList<>();
//        for (Optional<T> x : xs) {
//            if(x.isEmpty()) {
//                return Optional.empty();
//            }
//            ys.add(x.get());
//        }
//        return Optional.of(ys);
//    }
}

おまけ -- sequence関数の再実装

travarse関数からsequence関数を呼び出すのをやめましたが、実はこの新しいtravarse関数を使い、sequence関数を以下のように再実装できます。

public static <T> Optional<List<T>> sequence(List<Optional<T>> xs) {
    return traverse(xs, Function.identity()); // traverse(xs, x -> x)でも可
}

少し不思議かもしれませんが、ここでFunction.identity() (またはx -> x2) はOptional<T>を受け取りOptional<T>を返す関数であり、travarseの第二引数Function<T, Optional<R>> fは戻り値の型がOptional型であればいいので、上記のように書けます。3

説明用に、travarse関数の型パラメータをこの新sequence関数に合わせてみます。

public static <T> Optional<List<T>> traverse(List<Optional<T>> xs, Function<Optional<T>, Optional<T>> f) {
    List<T> ys = new ArrayList<>();
    for (Optional<T> x : xs) {
        Optional<T> o = f.apply(x); // fはFunction#identityなので、xとoは同一インスタンス
        if(o.isEmpty()) {
            return Optional.empty();
        }
        ys.add(o.get()); // 結局xの中身を詰めてるのと一緒
    }
    return Optional.of(ys);
}

最初に定義したsequence関数と同様、ただList<Optional<T>>の各要素のOptionalを外しているだけとわかります。

最後に

Javaではこういった関数を引数にするような汎用関数はあまり定義しないと思いますが(関数型インターフェースがたくさんあるので共通化しづらい)、何となく思い立ったので書いてみました。
実用性はともかく、頭の体操になって楽しいのでオススメです。

最後までお読み頂きありがとうございました。質問や不備についてはコメント欄かTwitter(@ka2_kamaboko)までお願いします。


  1. Optionalの中身の型に左右されない、汎用的な説明に繋げるため、OptionalIntではなOptionalを採用しています。 

  2. x -> xのように書いてもいいのですが、この書き方だと毎回Function<T, T>型のオブジェクトが生成されてしまうため、特に理由がなければFunction.identity()を使っておいた方が良いと思います。 

  3. 型パラメータのTが被っているので混乱するかもしれませんが、ここのtravarseのTとsequenceのTが指しているものは別の型です。travarseのTが、sequenceではOptional<T>にあたります。travarseの型パラメータT,Rは同じ型でもいいので、Function<Optional<T>, Optional<T>>型はtravarseの第二引数の要件を満たしています。 

Why do not you register as a user and use Qiita more conveniently?
  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
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