Generic Type Inference Not Working with Method Chaining

Generic type inference not working with method chaining?

Why ?

Because the type inference of generics types has not been expanded to chained invocation.

From the java tutorial on generics type inference:

The notion of what is a target type has been expanded to include method arguments.

That is why this code:

f(Map.empty());

compiles.

But this code doesn't because this is a chained invocation:

f(Map.empty().put(1,"A").put(2,"B"));

You can also find a small paragraph in the JSR-000335 Lambda Expressions for the JavaTM Programming Language Final Release for Evaluation (specifically part D):

There has been some interest in allowing inference to "chain": in a().b(), passing type information from the invocation of b to the invocation of a. This adds another dimension to the complexity of the inference algorithm, as partial information has to pass in both directions; it only works when the erasure of the return type of a() is fixed for all instantiations (e.g. List). This feature would not fit very well into the poly expression model, since the target type cannot be easily derived; but perhaps with additional enhancements it could be added in the future.

So maybe in Java 9.

Why does Java generics type inference break in chained method calls?

This answer from JDK Developers themselves sort of covers the same area. Just notice that Stuart Marks says : "It might be possible for the compiler to be enhanced to cover this case in a future release". Though the case there is around lambdas, this is not very different than you have. It's just the way compiler (at the moment) works. And we are "stuck" with this.

You can sort of look under the resolution of how a compiler thinks about return Stream.of("a").distinct(); and decides what type to use, via:

javac --debug=verboseResolution=all

which in an un-documented flag. If you compile with that flag, you will see some big output:

  with actuals: no arguments
with type-args: no arguments
candidates:
#0 applicable method found: Object()
DeleteMe.java:60: Note: resolving method of in type Stream to candidate 1
return Stream.of("a").distinct();
^
phase: BASIC
with actuals: String
with type-args: no arguments
candidates:
#0 not applicable method found: <T#1>of(T#1...)
(cannot infer type-variable(s) T#1
(argument mismatch; String cannot be converted to T#1[]))
#1 applicable method found: <T#2>of(T#2)
(partially instantiated to: (String)Stream<String>)
where T#1,T#2 are type-variables:
T#1 extends Object declared in method <T#1>of(T#1...)
T#2 extends Object declared in method <T#2>of(T#2)
DeleteMe.java:60: Note: Deferred instantiation of method <T>of(T)
return Stream.of("a").distinct();
^
instantiated signature: (String)Stream<String>
target-type: <none>
where T is a type-variable:
T extends Object declared in method <T>of(T)
DeleteMe.java:60: Note: resolving method distinct in type Stream to candidate 0
return Stream.of("a").distinct();
^
phase: BASIC
with actuals: no arguments
with type-args: no arguments
candidates:
#0 applicable method found: distinct()
where T is a type-variable:
T extends Object declared in interface Stream
DeleteMe.java:60: error: incompatible types: Stream<String> cannot be converted to Stream<CharSequence>
return Stream.of("a").distinct();
^
1 error

I guess the most important part is this : (partially instantiated to: (String)Stream<String>)

You can see that the resolution of what type T is, is done a method call basis; not of the whole chain of calls. If it would would, btw, this would complicate compilers works quite a lot. For a simple chain like this, things might look trivial, but it gets far, far more tricky and involved when there are many. Especially, when you find out about non-denotable types, which will even further complicate this.

Generic type parameters inference in method chaining

If this should really be a comment, please let me know and I'll break it in separate comments (it will probably not fit into a single one).

First poly expression is one that can have different types in different contexts. You declare something in contextA it has typeA; one in contextB and it has typeB.

What you are doing with your map creation via ImmutableMap.of("a", "b") or ImmutableMap.of(1,2) is such a thing. To be more precise Chapter 15.2 in the JLS says that this is actually a Class Instance Creation Expression poly expression.

So far we have established that A generic class instance creation is a poly expression. So that instance creation could have different types based on the context where it is used (and that obviously happens).

Now in your example with a ImmutableMap.builder things are not that difficult to infer (if you could chain builder and of for example) Builder is declared like this:

   public static <K, V> Builder<K, V> builder() {
return new Builder<K, V>();
}

and ImmutableMap.of like this:

   public static <K, V> ImmutableMap<K, V> of() {
return ImmutableBiMap.of();
}

Notice how both declare the same generic types K,V. This would probably be easy to pass from one method to another. But consider the case when your methods (let's say 10 methods) each declare different bound of the generic types, like:

<K,V> of()
....
<M extends T, R extends S> build()

And so on. And you can chain them. Information about types has to be passed from left to right and from right to left for the inference to work and as far as I can tell it would be very hard to do (besides being very complex).

Right now the way this works is that each poly expression is compiled one at a time from what I see (and your examples seems to prove that).

Java compiler not able to infer type on generic chain

On the lines of what Carlos has mentioned, there is one more way to resolve this. It can be used if you are sure of the suppliers of the implementation of Gen<T>.

Here, instead of the helper to call applyTo(), we define a factory method to create the instance of the Gen implementation and cast it to Gen<Base>, which in my opinion is safe for all practical purposes. Note that the factory method get() need not be static. The rest of the code remains unchanged from your sample.

public static void main( String[] args ){
Gen<Base> gen = (Gen<Base>) get();
gen.applyTo( Hija.EXAMPLE );
}

static Gen<? extends Base> get(){
/* Create the instance of the Gen interface implementation here. */
return new HijaGen();
}

Explanation

Gen<? extends Base> expects to refer to an instance of an implementation of Gen that uses a type that is either Base or an sub-type of it. Since a "sub-type" can be from one of many possible hierarchies from Base downward (as shown in the picture below), it cannot be sure that the parameter passed to applyTo() method is of the same child hierarchy path and not just a 'relative' with the same ancestral parent Base. That is why it won't allow a call to applyTo() with its reference being of Gen<? extends Base> for a parameter of with reference as Base.

However, when the reference is Gen<Base>, it knows that applyTo() will accept any parameter that is a child type of Base. And hence, it stops worrying about type mismatch.

I have tried to show what I mean by different child hierarchy paths. It is like the case of a family tree.

Sample Image

Chained generic type inferred closures in swift

functionOne needs to return something that has a chainedFunction instance method, which itself needs to return something that has an anotherChain instance method, and so on. For this, you need to create a type. I'll call it wrapper because I don't know your use case, but you should name it something more meaningful.

import Foundation

struct Wrapper<T> {
let value: T

// This replaces `function1`
init(_ value: T) { self.value = value }

func chainedFunction<R>(_ transform: (T) -> R) -> Wrapper<R> {
return Wrapper<R>(transform(self.value))
}

func anotherChain<R>(_ transform: (T) -> R) -> Wrapper<R> {
return Wrapper<R>(transform(self.value))
}

func lastInTheChain<R>(_ transform: (T) -> R) -> R {
return transform(self.value)
}
}

Wrapper("123")
.chainedFunction { string -> Int in
return Int(string)! + 456
}
.anotherChain {
[NSNumber(value: $0), String($0)]
}
.lastInTheChain {
print("value 1: \($0[0])")
print("value 2: \($0[1])")
}

Terminology: this Wrapper type is called a functor, because it defines a map method (here called chainedFunction and anotherChain, which are equivalent functions in the example). Using map, a closure which can be used to transform T to R (transform) can be used to transform a Wrapper<T> into a Wrapper<R>.



Related Topics



Leave a reply



Submit