Tricky min

この記事は Java Puzzlers Advent Calendar 2016 の 10 日目です。

import java.util.stream.*;
import java.util.function.*;

class Main {
    public static void main (String[] args) throws java.lang.Exception {
        System.out.println(
                Stream.of(2, 10, null, 100, 43)
                        .<UnaryOperator<Integer>>map(n -> m -> Integer.min(n, m))
                        .reduce(UnaryOperator.identity(), UnaryOperator::andThen)
                        .apply(Integer.MAX_VALUE));
	}
}
1. 2
3. 0
4. 2147483647
5. コンパイル時エラー
6. 実行時例外

解説は後ほど


nullの要素でぬるぽが出ると予想した人が多かったのではないでしょうか?
あるいは,Integer.minはnullを無視してくれて,2が出力されるのではと予想したもおられるかもしれません.

正解は5. コンパイル時エラーです.

出力されるコンパイルエラーは以下のようなものです.

|  Error:
|  不適合な型: ラムダ式の戻り型が不正です
|      型変数Vのインスタンスが存在しないので、java.util.function.Function<java.lang.Integer,V>はjava.util.function.UnaryOperator<java.lang.Integer>に適合しません
|                          .reduce(UnaryOperator.identity(), UnaryOperator::andThen)
|                                                            ^--------------------^

よく分からないですよね.UnaryOperatorのJavadocを見てみましょう.
UnaryOperator (Java Platform SE 8)

andThenメソッドはFunctionから継承しています.andThenメソッドのシグネチャは以下のようになってます.

default <V> Function<T,V> andThen(Function<? super R,? extends V> after)

簡単に言うとUnaryOperator::andThenは

<V>(UnaryOperator<Integer>, Function<? super Integer, ? extends V>) -> Function<Integer, V>

という型のメソッド参照になります.
一方で,reduceは(UnaryOperator, UnaryOperator) -> UnaryOperatorを求めるので「型が違うじゃねーか.そもそもV無いよ」と怒られているわけです.

修正は簡単です.andThenを使わずに自力でandThen相当の処理を書けばよいです.

import java.util.stream.*;
import java.util.function.*;

class Main {
    public static void main (String[] args) throws java.lang.Exception {
        System.out.println(
                Stream.of(2, 10, null, 100, 43)
                        .<UnaryOperator<Integer>>map(n -> m -> Integer.min(n, m))
                        .reduce(UnaryOperator.identity(),
//                                (before, after) -> n -> before.apply(after.apply(n)))
                                (before, after) -> before.andThen(after)::apply)
                        .apply(Integer.MAX_VALUE));
	}
}

ちなみにこれで実行すると,ぬるぽが出ます.
理由はInteger.minはintの値2つを取ります.
つまり,オート(アン)ボクシング発生してしまうためです.

ぬるぽが出ないようにするために,filterでnullを除外しましょう.

import java.util.stream.*;
import java.util.function.*;

class Main {
    public static void main (String[] args) throws java.lang.Exception {
        System.out.println(
                Stream.of(2, 10, null, 100, 43)
                        .filter(Objects::nonNull)
                        .<UnaryOperator<Integer>>map(n -> m -> Integer.min(n, m))
                        .reduce(UnaryOperator.identity(),
//                                (before, after) -> n -> before.apply(after.apply(n)))
                                (before, after) -> before.andThen(after)::apply)
                        .apply(Integer.MAX_VALUE));
	}
}

これで2が出力されます.

ところで,minを求めるのにこんな面倒臭いことはしないですよね.Stream.minを使うともっとシンプルに書けます.

import java.util.stream.*;
import java.util.function.*;

class Main {
    public static void main (String[] args) throws java.lang.Exception {
        System.out.println(
                Stream.of(2, 10, null, 100, 43)
                        .filter(Objects::nonNull)
                        .min(Comparator.naturalOrder())
                        .orElse(Integer.MAX_VALUE));
	}
}