Cannot use Java 8 method with lambda arguments without specifying type arguments
Asked Answered
C

2

15

I made a method with type arguments, returning a generic type using these type arguments, and taking Function arguments which also depends on the type arguments. When I use lambdas as arguments, the compiler forces me to specify the type arguments of the method, which feels wrong.

I am designing a utility class with methods to use with Stream.flatMap. It maps every kind of collection entry to a FlatEntry which contains a key and value element, and can do this on multiple levels with a builder. The affected method is flatEntryMapperBuilder. Here is the code:

import java.util.function.Function;
import java.util.stream.Stream;

public class GdkStreams
{
    public static <T, K, V> Function<T, Stream<FlatEntry<K, V>>> flatEntryMapper(Function<T, K> keyMapper,
                                                                                 Function<T, Stream<V>> valueMapper)
    {
        return input -> {
            K key = keyMapper.apply(input);
            return valueMapper.apply(input).map(value -> new FlatEntry<>(key, value));
        };
    }

    public static <T, K, V> FlatEntryMapperBuilder<T, K, V> flatEntryMapperBuilder(Function<T, K> keyMapper,
                                                                                   Function<T, Stream<V>> valueMapper)
    {
        return new FlatEntryMapperBuilder<>(keyMapper, valueMapper);
    }

    public static class FlatEntryMapperBuilder<T, K, V>
    {
        private Function<T, K>         keyMapper;

        private Function<T, Stream<V>> valueMapper;

        private FlatEntryMapperBuilder (Function<T, K> keyMapper, Function<T, Stream<V>> valueMapper)
        {
            this.keyMapper = keyMapper;
            this.valueMapper = valueMapper;
        }

        public Function<T, Stream<FlatEntry<K, V>>> build()
        {
            return flatEntryMapper(keyMapper, valueMapper);
        }

        public <K2, V2> FlatEntryMapperBuilder<T, K, FlatEntry<K2, V2>> chain(Function<V, K2> keyMapper2,
                                                                              Function<V, Stream<V2>> valueMapper2)
        {
            return new FlatEntryMapperBuilder<>(keyMapper,
                                                valueMapper.andThen(stream -> stream.flatMap(flatEntryMapper(keyMapper2,
                                                                                                             valueMapper2))));
        }
    }

    public static class FlatEntry<K, V>
    {
        public final K key;

        public final V value;

        public FlatEntry (K key, V value)
        {
            this.key = key;
            this.value = value;
        }
    }
}

The problem comes with its usage. Say I have:

Map<String, Set<String>> level1Map;

I can map every element in the sub Sets to a FlatEntry by doing:

level1Map.entrySet().stream().flatMap(GdkStreams.flatEntryMapper(Entry::getKey, entry -> entry.getValue().stream()));

And it works just fine. But when I try to do this:

level1Map.entrySet()
         .stream()
         .flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());

The eclipse (Mars 4.5.0) compiler breaks with:

- The type Map.Entry does not define getKey(Object) that is applicable here
- The method getValue() is undefined for the type Object
- Type mismatch: cannot convert from GdkStreams.FlatEntryMapperBuilder<Object,Object,Object> to 
 <unknown>

And javac (1.8.0_51) breaks with:

MainTest.java:50: error: incompatible types: cannot infer type-variable(s) T,K#1,V#1
                 .flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());
                                                           ^
    (argument mismatch; invalid method reference
      method getKey in interface Entry<K#2,V#2> cannot be applied to given types
        required: no arguments
        found: Object
        reason: actual and formal argument lists differ in length)
  where T,K#1,V#1,K#2,V#2 are type-variables:
    T extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
    K#1 extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
    V#1 extends Object declared in method <T,K#1,V#1>flatEntryMapperBuilder(Function<T,K#1>,Function<T,Stream<V#1>>)
    K#2 extends Object declared in interface Entry
    V#2 extends Object declared in interface Entry
MainTest.java:50: error: invalid method reference
                 .flatMap(GdkStreams.flatEntryMapperBuilder(Entry::getKey, entry -> entry.getValue().stream()).build());
                                                            ^
  non-static method getKey() cannot be referenced from a static context
  where K is a type-variable:
    K extends Object declared in interface Entry
2 errors

If I replace Entry::getKey by entry -> entry.getKey(), javac changes its output drastically:

MainTest.java:51: error: cannot find symbol
                 .flatMap(GdkStreams.flatEntryMapperBuilder(entry -> entry.getKey(), entry -> entry.getValue().stream()).build());

                                                                          ^
  symbol:   method getKey()
  location: variable entry of type Object
MainTest.java:51: error: cannot find symbol
                 .flatMap(GdkStreams.flatEntryMapperBuilder(entry -> entry.getKey(), entry -> entry.getValue().stream()).build());

                                                                                                   ^
  symbol:   method getValue()
  location: variable entry of type Object
2 errors

It compiles fine by specifying type parameters, which is what I expected:

level1Map.entrySet()
         .stream()
         .flatMap(GdkStreams.<Entry<String, Set<String>>, String, String> flatEntryMapperBuilder(Entry::getKey,
                                                                                                 entry -> entry.getValue()
                                                                                                               .stream())
                            .build());

or specifying one of the arguments type parameters:

Function<Entry<String, Set<String>>, String> keyGetter = Entry::getKey;
level1Map.entrySet()
         .stream()
         .flatMap(GdkStreams.flatEntryMapperBuilder(keyGetter, entry -> entry.getValue().stream()).build());

But this is clumsy! Imagine now how clumsy it would be to write all type parameters with 2 levels in the map, using the chain method (which is my target usage):

Map<String, Map<String, Set<String>>> level2Map;

I have read many other questions about lambdas and generics type inference but none is answering my particular case.

Am I missing something? Can I correct my API so that its usage is less clumsy, or am I stuck with always specifying type arguments? Thanks!

Clipped answered 8/10, 2015 at 12:49 Comment(11)
In your actual code you have two more type arguments k2,v2 , should it be K and V?Benny
The chain method has more type arguments, but for the presented use case it is not used (I included it for completeness sake). The problem occurs only by using flatEntryMapperBuilder. Removing chain from the type FlatEntryMapperBuilder does not change the problem.Clipped
This is a known limitation of Java 8’s type inference: it doesn’t work with chained method invocations like genericFactoryMethod().build().Fenn
Awww... ok. I guess I am stuck with specifying type arguments then. Thanks!Clipped
But you don’t need the builder as you can chain the function in-place: .stream().flatMap(flatEntryMapper(Entry::getKey, entry -> entry.getValue().stream().flatMap(flatEntryMapper(…))))Fenn
Yep, that's what I did first. What bugged me is that it creates so many nested parenthesis, reducing readability. That is why I would like to write: .stream().flatMap(flatEntryMapperBuilder(functionFoo1, functionBar1).chain(functionFoo2, functionBar2).chain(functionFoo3, functionBar3).build()) instead of: .stream().flatMap(flatEntryMapper(functionFoo1, functionBar1.andThen(stream -> stream.flatMap(flatEntryMapper(functionFoo2, functionBar2.andThen(stream -> stream.flatMap(flatEntryMapper(functionFoo3, functionBar3))))))))Clipped
You shouldn’t need function composition. A .stream().flatMap(flatEntryMapper(functionFoo1, functionBar1)) .flatMap(flatEntryMapper(functionFoo2, functionBar2)) .flatMap(flatEntryMapper(functionFoo3, functionBar3)) should do as well…Fenn
When chaining like that, inside functionFoo2, you have to instantiate the FlatEntry manually. This makes it harder for each level. E.g.:Clipped
flatEntry -> flatEntry.value.getValue() .stream() .map(value -> new FlatEntry<>(flatEntry.value.getKey(), value)))Clipped
Really? I thought that’s what flatEntryMapper(…) does. Note that in my code example, it is still being used.Fenn
Yeah it does, but the idea is to convert e.g. a Map<String, Map<String, Set<String>>> into a stream of FlatEntry<String, FlatEntry<String, String>>. flatEntryMapper does its job nicely for one level, but if you have multiple levels you have to chain it like you mentionned in the 5th comment, with nested calls. If you write it not nested, you have to use a complicated function that takes a FlatEntry as argument and recreate one, because the result of the previous call left you with a Stream<FlatEntry<...>>.Clipped
C
10

Holger had the best answer in the comment section in my opinion:

This is a known limitation of Java 8’s type inference: it doesn’t work with chained method invocations like genericFactoryMethod().build().

Thanks! About my API, I will specify the functions before using them as arguments, like this:

Function<Entry<String, Set<String>>, String> keyMapper = Entry::getKey;
Function<Entry<String, Set<String>>, Stream<String>> valueMapper = entry -> entry.getValue().stream();

EDIT: I redesigned the API thanks to Holger's comments (thanks again!). It keeps the original element instead of a key, along with the flattened value.

public static <T, R> Function<? super T, Stream<FlatEntry<T, R>>> flatEntryMapper(Function<? super T, ? extends Stream<? extends R>> mapper)
{
    return element -> mapper.apply(element).map(value -> new FlatEntry<>(element, value));
}

public static class FlatEntry<E, V>
{
    /** The original stream element */
    public final E element;

    /** The flattened value */
    public final V value;

    private FlatEntry (E element, V value)
    {
        this.element = element;
        this.value = value;
    }
}

It is chainable, starting with level 2 the mapper has to process a FlatEntry. The usage is similar to a simple flatMap:

Map<String, Map<String, Map<String, Set<String>>>> level3Map;

// gives a stream of all the flattened values
level3Map.entrySet()
         .stream()
         .flatMap(entry -> entry.getValue().entrySet().stream())
         .flatMap(entry -> entry.getValue().entrySet().stream())
         .flatMap(entry -> entry.getValue().stream());

// gives a stream of FlatEntries with flattened values and all their original elements in nested FlatEntries
level3Map.entrySet()
         .stream()
         .flatMap(GdkStreams.flatEntryMapper(entry -> entry.getValue().entrySet().stream()))
         .flatMap(GdkStreams.flatEntryMapper(flatEntry -> flatEntry.value.getValue().entrySet().stream()))
         .flatMap(GdkStreams.flatEntryMapper(flatEntry -> flatEntry.value.getValue().stream()));
Clipped answered 8/10, 2015 at 15:3 Comment(1)
I would say that it's a problem with the inference in the Java 8 compiler. Take the following as an example: final Set<String> expectedDaoData3 = Collections.unmodifiableSet( Arrays.asList("a", "b", "c").stream().collect(Collectors.toCollection(LinkedHashSet::new))); It compiles with Eclipse Compiler for Java, but not OpenJDK, which complains with: required: java.util.Set<? extends T> found: java.util.Collection<java.lang.String> reason: cannot infer type-variable(s) EEduce
N
4

One way to provide enough type information to the compiler is to declare an explicit type of one of the lambda argument. This is in the same spirit as your answer but a little more compact, since you only have to provide the type of the argument, not the whole function.

This looks pretty okay for the one-level map:

level1Map.entrySet().stream()
    .flatMap(GdkStreams.flatEntryMapperBuilder(
        (Entry<String, Set<String>> entry) -> entry.getKey(), 
        entry -> entry.getValue().stream()).build());

The two-level map is on the border to the grotesque, however:

level2Map.entrySet().stream()
    .flatMap(GdkStreams.flatEntryMapperBuilder(
        (Entry<String, Map<String, Set<String>>> entry1) -> entry1.getKey(), 
        entry1 -> entry1.getValue().entrySet().stream()
            .flatMap(GdkStreams.flatEntryMapperBuilder(
                (Entry<String, Set<String>> entry2) -> entry2.getKey(), 
                entry2 -> entry2.getValue().stream()).build())).build());
Negatron answered 8/10, 2015 at 15:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.