Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Failure to infer type at an unexpected place (possible javac bug?) [duplicate]

I am trying to do what seems to be a relatively basic thing in the new JDK 8 land of functional programming, but I can't get it to work. I have this working code:

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());
   }
}

It takes a list of numbers and produces a list of functions that can print them out. However, the explicit cast to Callable seems redundant. It seems to me and to IntelliJ. And we both agree that this should also work:

List<Callable<Object>> checks = l.stream().
       map(n -> () -> {
          System.out.println(n);
          return null;
       }).
       collect(Collectors.toList());

However I get an error:

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
like image 373
MK. Avatar asked Nov 11 '14 19:11

MK.


4 Answers

You hit a limitation of Java 8’s target typing which applies to the receiver of a method invocation. While target typing works (most of the times) for parameter types it does not work for the object or expression on which you invoke the method.

Here, l.stream(). map(n -> () -> { System.out.println(n); return null; }) is the receiver of the collect(Collectors.toList()) method invocation, so the target type List<Callable<Object>> is not considered for it.

It’s easy to prove that nested lambda expressions work if the target type is know, e.g.

static <T> Function<T,Callable<Object>> toCallable() {
    return n -> () -> {
        System.out.println(n); 
        return null;
    };
}

works without problems and you can use it to solve your original problem as

List<Callable<Object>> checks = l.stream()
    .map(toCallable()).collect(Collectors.toList());

You can also solve the problem by introducing a helper method which changes the role of the first expression from method receiver to a parameter

// 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);
}

and rewrite the original expression as

List<Callable<Object>> checks = collect(l.stream().map(
    n -> () -> {
        System.out.println(n); 
        return null;
    }), Collectors.toList());

This does not reduce the complexity of the code but can be compiled without any problems. For me, it’s a déjà vu. When Java 5 and Generics came out, programmers had to repeat the type parameters on new expressions while simply wrapping the expression into a generic method proved that inferring the type is no problem. It took until Java 7 before programmers were allowed to omit these unnecessary repetition of the type arguments (using the “diamond operator”). Now we have a similar situation, wrapping an invocation expression into another method, turning the receiver into a parameter, proves that this limitation is unnecessary. So maybe we get rid of this limitation in Java 10…

like image 79
Holger Avatar answered Nov 15 '22 03:11

Holger


I ran into this same issue and was able to solve it by explicitly specifying the generic type-parameter to map like so:

List<Callable<Object>> checks = l.stream().
   <Callable<Object>>map(n -> () -> {
      System.out.println(n); 
      return null;
   }).
   collect(Collectors.toList());
like image 29
Vivin Paliath Avatar answered Nov 15 '22 02:11

Vivin Paliath


I haven't yet delved into the exact rules for how type inference works with lambdas. From a general language design standpoint, though, it isn't always possible to write language rules that allow the compiler to figure out everything we think it should. I've been a compiler maintainer for an Ada-language compiler and I'm familiar with many of the language design issues there. Ada uses type inference in a lot of cases (where the type of a construct can't be determined without looking at the entire expression containing the construct, which I think is the case with this Java lambda expression also). There are some language rules that cause compilers to reject some expressions as ambiguous when, in theory, there really is only one possible interpretation. One reason, if I recall correctly, is that somebody found a case where a rule that would have let the compiler figure out the correct interpretation would have required the compiler to make 17 passes through an expression in order to interpret it correctly.

So while we may think a compiler "should" be able to figure something out in a particular case, it may just plain be unfeasible.

like image 24
ajb Avatar answered Nov 15 '22 02:11

ajb


Firstly you have to know how compiler get a lambda expression's type. It is achieved by target typing, which means the type of the variable that you assign the lambda expression to. In your case, if you

Function<Integer, Callable<Object>> fn = n -> () -> { System.out.println(n); return null; }

This is how the lambda get its type: Function<Integer, Callable<Object>>

Then you have to look at the type inference in generic type: The return type of map is <R> Stream<R>, R would be determined by type of the parameter that you passed into the function. If you map(x->"some string"), then the result is Stream<String>. Now this is the problem, R's type the lambda's type. But lambda needs a target type, which is the variable R.

The working code works because it explicitly cast the lambda to a type.

like image 29
Lukaka Avatar answered Nov 15 '22 04:11

Lukaka