Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Code compiles in Eclipse but not javac: curried lambdas with functional subinterface. Which is correct?

I developed some code in Eclipse, tested it successfully, pushed it to our Jenkins CI server, and got an email that Maven was choking with a Java compile error. I subsequently isolated the problem and created the following minimal example showing the issue:

import java.util.List;
import java.util.function.Function;

class MinimalTypeFailureExample {
    public static void main(String[] args) {
        List<String> originalList = null;  // irrelevant
        List<IntToByteFunction> resultList = transform(originalList,
                outer -> inner -> doStuff(inner, outer));
        System.out.println(resultList);
    }

    static <F, T> List<T> transform(List<F> originalList,
            MyFunction<? super F, ? extends T> function) {
        return null;  // irrelevant
    }

    static Byte doStuff(Integer inner, String outer) {
        return null;  // irrelevant
    }
}

@FunctionalInterface
interface MyFunction<F, T> extends Function<F, T> {
    @Override
    T apply(F input);
}

@FunctionalInterface
interface IntToByteFunction {
    Byte applyIntToByte(Integer inner);
}

In Eclipse, this code compiles without error and appears to execute as intended. However, compiling with javac gives the following error:

MinimalTypeFailureExample.java:7: error: incompatible types: cannot infer type-variable(s) F,T
                List<IntToByteFunction> resultList = transform(originalList, outer -> inner -> doStuff(inner, outer));
                                                              ^
    (argument mismatch; bad return type in lambda expression
      T is not a functional interface)
  where F,T are type-variables:
    F extends Object declared in method <F,T>transform(List<F>,MyFunction<F,? extends T>)
    T extends Object declared in method <F,T>transform(List<F>,MyFunction<F,? extends T>)
1 error

Changing the argument type of transform() from MyFunction to Function, or removing the wildcard ? extends in the argument type, makes the example code compile in javac.

Clearly, either Eclipse or javac is in violation of the Java Language Specification. The question is, do I file the bug report on Eclipse or javac? The type inference rules for generic lambdas are so complex that I have no idea whether this program is legal Java or not according to the JLS.

Motivation note

In the original code, transform() was Guava's com.google.common.collect.Lists.transform(). The MyFunction interface was Guava 's com.google.common.base.Function interface, which extends java.util.function.Function for historical reasons.

The purpose of this code was to create a view of a list of a first type as a list of a second type. The second type was a functional interface type and I wanted to populate the output list with functions of this type constructed based on the values in the input list—hence the curried lambda expression.

Version info for reproducibility

Eclipse versions tested:

  • 2018-09 (4.9.0) Build id: 20180917-1800
  • 2019-03 RC1 (4.11 RC1) Build id: 20190307-2044

javac versions tested:

  • 1.8.0_121
  • JDK 10.0.1 via the JDoodle online Java compiler
like image 979
Aaron Rotenberg Avatar asked Mar 15 '19 19:03

Aaron Rotenberg


2 Answers

It looks like you run into JDK bug JDK-8156954 which has been fixed in Java 9 but not in Java 8.

It is a bug of Java 8 javac because in your example all variable types of the transform method can be inferred without violating the Java language specification as follows:

  • F: String (via first parameter originalList of type List<String>)
  • T: IntToByteFunction (via return type List<IntToByteFunction>)

These inferred variable types are compatible with the type of the second parameter, the chained lambda expression:

  • outer -> inner -> doStuff(inner, outer) resolves (with doStuff(Integer, String) to
  • String -> Integer -> doStuff(Integer, String) resolves to
  • String -> Integer -> Byte is compatible with
  • String -> IntToByteFunction is compatible with
  • MyFunction<? super String, ? extends IntToByteFunction>

Your example can be minimized further:

import java.util.function.Function;

class MinimalTypeFailureExample {

    void foo() {
        transform((Function<Integer, String>)null, o -> i -> {return "";});
    }

    <T, F> void transform(F f, MyFunction<T, ? extends F> m) {}

}

@FunctionalInterface
interface MyFunction<T, R> extends Function<T, R> {
    @Override
    R apply(T t);
}

MyFunction overrides the same with the same (R apply(T t);). If Function instead of MyFunction is used or if MyFunction extends Function but without @Override R apply(T t); then the error disappears. Also with F instead of ? extends F the error disappears.

Even if your example differs from the example in the mentioned bug, it can be assumed that it is the same bug because it is the only "argument mismatch; bad return type in lambda expression bug that has been fixed in Java 9 but not in Java 8 and that occurs only with lambda functions in combination with Java Generics.

like image 196
howlger Avatar answered Oct 03 '22 01:10

howlger


I tried the example code with javac 11.0.2 and received no error. That would suggest that the bug may have been in javac and is fixed in recent versions. I am slightly surprised at this because as mentioned I did try testing JDK 10 in an online interface.

I am open to other answers that provide more details on the specific problem, such as a JDK bug number for the issue.

As a workaround to make the code compile in JDK 8, an explicit cast can be added to the inner lambda expression:

List<IntToByteFunction> resultList = transform(originalList,
        outer -> (IntToByteFunction) inner -> doStuff(inner, outer));
like image 21
Aaron Rotenberg Avatar answered Oct 02 '22 23:10

Aaron Rotenberg