Overload resolution simplification

Zhong Yu zhong.j.yu at gmail.com
Sun Aug 11 10:32:12 PDT 2013


On Sun, Aug 11, 2013 at 4:50 AM, maurizio cimadamore
<maurizio.cimadamore at oracle.com> wrote:
> On 10-Aug-13 10:58 PM, Remi Forax wrote:
>>
>> It's not less magic. It's no magic.
>
> This is a bit unfair. I think there's still plenty of magic going on. Note
> that the path that led us there is that we were uncomfortable in having the
> compiler to pick one method over another because of some weird error in some
> implicit lambdas. Now - if you have something like:
>
> m(Function<String, Integer> f)
> m(IntFunction<String> f)
>
> And a lambda like
>
> m(x->1)
>
> it could be trivial to see that the second method is the one you want.
>
> On the other hand, consider the following overload of _non generic_ methods:
>
> m(Function<String, Integer> f)
> m(IntFunction<Integer> f)
>
> If we start accepting this (overload between non-generic methods - so
> everything is ok, no?) - then we open up can of worms in which an error in
> one speculative type-check will cause the method not to be applicable - i.e.
> :
>
> m(x->x.length()) //no member length() in Integer, so the first method is
> selected
> m(x->x.intValue()) //no member intValue() in String, so the second method is
> selected
>
> This was what the compiler was doing - and EG expressed some concerns with
> this - and for a good reason I think. A seemingly innocuous refactoring of a
> lambda body can trigger a change in overload resolution of the enclosing
> method call.
>
> In other words, even when there's an overload between non-generic methods,
> it gets very muddy very soon as soon as you consider all consequences of
> 'adding more magic'. Suddenly you have to start thinking about errors in the
> lambda body to reason about method applicability, and that's brittle (in
> fact, even though an earlier version of flatMap was able to disambiguate
> because of this, we decided NOT to rely on this as too magic/brittle).
>
> So, the only meaningful question here is - where should we draw the line? I
> see three options:
>
> 1) Allow non-generic overloads, provided each overload forces same choice on
> lambda parameter types
> 2) Expand on 1a, implementing a more complex logic that would also work on
> generic methods
> 3) Disallow implicit lambdas when overloads of the same arity are available

If you give people (1), they'll want (2), and be disappointed that (2)
isn't available.

But if (1) is not available, common use cases suffer immediately,
everyday application code will look ugly and repetitive -
stream.mapToInt( :: anIntFunc )


>
> I think 2 was considered also too complex, as the analysis would have to
> treat inference variables that depend on the return type in a different way
> (i.e. Comparator.comparing won't work) - and that's surprising. So that's
> mostly a dead end.
>
> The choice is between 1 and 3. As a result of the current simplification, we
> are in 3. Now, I'm not totally against 1 - I think it's a legitimate
> position, but there are things about that approach that worry me:
>
> *) inconsistencies between non-generic vs. generic methods

It's also inconsistent if this overloading works fine
    m( ()->A )
    m( ()->B )
yet this doesn't
    m( X->A )
    m( X->B )

It is also inconsistent if this is ok
    m( X->A )
    m(x->expr)
and this is not
    m( X->A )
    m( X->B )
    m(x->expr)
yet inexplicably, this is ok again
    m((X x)->expr)
(well you can explain it, but it's hard to accept)

I think it would be consistent to do this: if the lambda parameter
type can uniquely determined from context though a few simple rules,
use it; otherwise error.


> *) hard to parse when parameter types are ordered in different ways with
> different SAM types (see my earlier example on this list)
> *) will miss ambiguities when passing a lambda to _semantically_ unrelated
> overloads - i.e.
>
> m(Function<String, Integer>)
> m(Predicate<String>)
>
> This example passes the rule that all overloads force same choice on lambda
> parameters. Should we accept a method call like the following?
>
> m(x->foo(x))
>
> Well, if we follow that scheme, the answer is yes - and the candidate is
> chosen depending on the return type of foo(String). I think that this

m( (String x)->foo(x) ) will suffer from the same difficulty, correct?
Therefore this example cannot be used against m( x->foo(x ).

> choice, while legitimate, is already 'too magic' - we have to very different
> targets there - with entirely different semantics; I think in such cases I
> would be more comfortable in having a more explicit form of disambiguation
> at the call site.
>
> Last point - it looks to me that all cases in which we would prefer 1 over 3
> have to do with primitive specialization of SAM. On the one hand, the Java
> type-system features this split between primitive and reference types - so
> that's what we get; on the other hand, if (big if :-)) we had some form of
> primitive vs. reference unification, how many cases would be left that 1

If it takes 5 years to get there, that's still a very long time,
during which (1) will be very useful to a lot of people.

Even if you care about long term purity of the language, is (1) really
that hackish and exceptional?

> could support that 3 cannot? Are we comfortable in _permanently_ adding more
> complexity to an already very complex area of the language, that could then
> be useless (at best, or even fire back) if/when the type-system is made more
> uniform?


>
> Maurizio
>
>
>
>


More information about the lambda-spec-observers mailing list