関数型プログラミングの新しいJDK8ランドで比較的基本的なことをしようとしていますが、それを機能させることができません。私はこの作業コードを持っています:
import Java.util.*;
import Java.util.concurrent.*;
import Java.util.stream.*;
public class so1 {
public static void main() {
List<Number> l = new ArrayList<>(Arrays.asList(1, 2, 3));
List<Callable<Object>> checks = l.stream().
map(n -> (Callable<Object>) () -> {
System.out.println(n);
return null;
}).
collect(Collectors.toList());
}
}
数値のリストを受け取り、それらを出力できる関数のリストを生成します。ただし、Callableへの明示的なキャストは冗長に見えます。私と IntelliJ のようです。そして、私たちはこれも機能するはずであることに同意します。
List<Callable<Object>> checks = l.stream().
map(n -> () -> {
System.out.println(n);
return null;
}).
collect(Collectors.toList());
ただし、エラーが発生します。
so1.Java:10: error: incompatible types: cannot infer type-variable(s) R
List<Callable<Object>> checks = l.stream().map(n -> () -> {System.out.println(n); return null;}).collect(Collectors.toList());
^
(argument mismatch; bad return type in lambda expression
Object is not a functional interface)
where R,T are type-variables:
R extends Object declared in method <R>map(Function<? super T,? extends R>)
T extends Object declared in interface Stream
1 error
メソッド呼び出しのreceiverに適用されるJava8のターゲット型指定の制限に達しました。ターゲット型付けは(ほとんどの場合)パラメーター型に対しては機能しますが、メソッドを呼び出すオブジェクトまたは式に対しては機能しません。
ここで、l.stream(). map(n -> () -> { System.out.println(n); return null; })
はcollect(Collectors.toList())
メソッド呼び出しの受信者であるため、ターゲットタイプはList<Callable<Object>>
は考慮されていません。
ターゲットタイプがわかっている場合、ネストされたラムダ式が機能することを証明するのは簡単です。
static <T> Function<T,Callable<Object>> toCallable() {
return n -> () -> {
System.out.println(n);
return null;
};
}
問題なく動作し、元の問題を解決するために使用できます。
List<Callable<Object>> checks = l.stream()
.map(toCallable()).collect(Collectors.toList());
最初の式の役割をメソッドレシーバーからパラメーターに変更するヘルパーメソッドを導入することで、問題を解決することもできます。
// turns the Stream s from receiver to a parameter
static <T, R, A> R collect(Stream<T> s, Collector<? super T, A, R> collector) {
return s.collect(collector);
}
元の式を次のように書き直します
List<Callable<Object>> checks = collect(l.stream().map(
n -> () -> {
System.out.println(n);
return null;
}), Collectors.toList());
これはコードの複雑さを軽減しませんが、問題なくコンパイルできます。私にとって、それは既視感です。 Java 5とGenericsが登場したとき、プログラマーはnew
式で型パラメーターを繰り返す必要がありましたが、式をジェネリックメソッドにラップするだけで、型の推測に問題がないことが証明されました。プログラマーが型引数のこれらの不必要な繰り返しを(「ダイヤモンド演算子」を使用して)省略できるようになるまで、Java7までかかりました。同様の状況が発生しました。呼び出し式を別のメソッドにラップし、レシーバーをパラメーターに変換すると、この制限が不要であることがわかります。したがって、Java10でこの制限を取り除くかもしれません…
私はこれと同じ問題に遭遇し、次のようにジェネリック型パラメーターをmap
に明示的に指定することで問題を解決することができました。
List<Callable<Object>> checks = l.stream().
<Callable<Object>>map(n -> () -> {
System.out.println(n);
return null;
}).
collect(Collectors.toList());
型推論がラムダでどのように機能するかについての正確なルールについては、まだ詳しく調べていません。ただし、一般的な言語設計の観点からは、コンパイラーが必要と思われるすべてを理解できるようにする言語ルールを作成できるとは限りません。私はAda言語コンパイラのコンパイラメンテナであり、そこでの言語設計の問題の多くに精通しています。 Adaは多くの場合に型推論を使用します(コンストラクトを含む式全体を見ないとコンストラクトのタイプを判別できない場合、これはJava lambdaの場合だと思います理論的には、考えられる解釈が1つしかない場合に、コンパイラが一部の式をあいまいなものとして拒否する言語規則がいくつかあります。1つの理由は、正しく思い出せば、誰かがコンパイラーに正しい解釈を理解させるには、コンパイラーが式を正しく解釈するために式を17回通過する必要があります。
したがって、コンパイラは特定の場合に何かを理解できるはずだと思うかもしれませんが、それは単に実行不可能かもしれません。
まず、コンパイラがラムダ式の型を取得する方法を知る必要があります。これは、ラムダ式を割り当てる変数のタイプを意味するターゲットタイピングによって実現されます。あなたの場合、あなたが
_Function<Integer, Callable<Object>> fn = n -> () -> { System.out.println(n); return null; }
_
これがラムダがそのタイプを取得する方法です:_Function<Integer, Callable<Object>>
_
次に、ジェネリック型の型推論を確認する必要があります。マップの戻り値の型は_<R> Stream<R>
_であり、Rは関数に渡したパラメーターの型によって決定されます。 map(x->"some string")
の場合、結果は_Stream<String>
_になります。これが問題です。Rはラムダの型です。ただし、ラムダにはターゲット型が必要です。これは変数Rです。
ラムダを型に明示的にキャストするため、機能するコードは機能します。