Inference Bug related to parametric polymorphism unification
Maurizio Cimadamore
maurizio.cimadamore at oracle.com
Mon Jun 10 11:33:35 UTC 2019
Hi John
I've been trying to reproduce, but for me the example fails even in 8...
That said, I agree that there could be something fishy here; I did some
experiments with variations of your example, and it looks like javac can
undrestand what the typing of `destructure(Pair::new, entry)` is, in
isolation - you can see that by adding this statement:
Object o = (String)destructure(Pair::new, entry);
This will trigger this error:
error: incompatible types: Pair<String,String> cannot be converted to String
Object o = (String)destructure(Pair::new, entry);
Which seems to confirm that javac is indeed capable of inferring A and B
to String and take it from there.
My suspicion is that when you nest these expressions inside each other,
you create a 'bigger' inference context, where two expressions cannot be
evaluated (we call them 'stuck'):
* Pair::new (indirect method reference)
* (x, y) -> x + y (implicit lambda)
To break this tie, the compiler has to choose which inference variable
to instantiate first: the outer { A, B }, or the innermost ones? This
choice is, at its core, driven by the spec 18.5.2 (see [1]). In your
example we have these inference variables:
* A#inner, B#inner and C#inner (from innermost invocation of 'destructure')
* A#outer, B#outer and C#outer (from outermost invocation of 'destructure')
This analysis is based on the concept of input/output inference variable
- in this case:
* A#inner,B#inner influence C#inner
* A#outer,B#outer influence C#outer
So far, if we had to choose between these two constraints, the choice
would be non-deterministic (as the two input/output constraints are
mirroring each other). But there's another dependency here:
* C#inner <: Entry<A#outer, B#outer>
Now, from a programmer perspective, looking at the way the code is
written, it's clear we'd like solution of C#inner to influence A#outer,
B#outer. But I don't think that's the way the spec works. In this case
the spec says that C#inner 'depends' on A#outer, C#outer.
So we really have no way
That's because the result of the innermost call to 'destructure' is
passed to the outermost call, so the inferred type for C#inner can
affect the inferred Entry<A#outer, B#outer>.
So, we are basically left with what I think is an non-deterministic
choice between eagerly resolving { A#inner, B#inner } or { A#outer,
B#outer }. Unfortunately, picking the former makes the code pass,
whereas picking the latter makes compilation fail (because at that time
we don't know any better than just instantiating A#outer and B#outer to
Object).
If this is the issue, inference could to a better job, by either looking
at presence of other 'more meaningful' bounds on {A#inner, B#inner}
(although defining 'more meaningful might be problematic). Or it could
just give precedence to innermost variables in case of a tie, which is a
rule that I think programmers will find natural. Currently though, these
'extensions' are not part of the JLS - so the behavior here is,
essentially, unspecified.
At the same time, there's also an argument to be had as to whether the
code here isn't inherently buggy - essentially, { A, B, C } in the
'destructure' method are all independent variables so, in a way, the
problem is *underconstrained* (e.g. there's no real objective choice for
which variable to solve first, other than resorting to heuristics, whose
mileage can vary).
Note that your second call works simply because, by adding the explicit
type param on the method reference receiver, you make the method
reference 'direct' meaning its type is evaluated *ahead* of the lambda.
Basically you told the compiler which of the two 'stuck' expression
should be solved first.
@Dan - is this something that has been discussed before in spec-land?
Cheers
Maurizio
[1] -
https://docs.oracle.com/javase/specs/jls/se12/html/jls-18.html#jls-18.5.2.2
On 06/06/2019 20:31, John Napier wrote:
> Hi all,
>
> The following code compiles against oracle64-1.8.0.162 but fails on
> both openjdk64-11.0.2 and oracle64-12.0.1:
>
> public static class Example {
>
> public static class Pair<A, B> implements Map.Entry<A, B> {
> private final A a;
> private final B b;
>
> public Pair(A a, B b) {
> this.a = a;
> this.b = b;
> }
>
> @Override
> public A getKey() {
> return a;
> }
>
> @Override
> public B getValue() {
> return b;
> }
>
> @Override
> public B setValue(B value) {
> throw new UnsupportedOperationException();
> }
> }
>
> public static <A, B, C> C destructure(BiFunction<A, B, C> fn,
> Map.Entry<A, B> entry) {
> return fn.apply(entry.getKey(), entry.getValue());
> }
>
> public static void main(String[] args) {
> Map.Entry<String, String> entry = new Pair<>("foo", "bar");
>
> // ill-typed
> String inferred = destructure((x, y) -> x + y,
> destructure(Pair::new, entry));
>
> // well-typed
> String ascribed = destructure((x, y) -> x + y,
> destructure(Pair<String, String>::new, entry));
> }
> }
>
> Before submitting more bugs of this same ilk, albeit with slight
> variations in occurrence, I must ask: this is not the intentional
> modern behavior, correct? Inference should still work as illustrated
> above, right?
>
> Cheers,
>
> -John
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.java.net/pipermail/compiler-dev/attachments/20190610/b6c598e7/attachment.html>
More information about the compiler-dev
mailing list