Overload resolution simplification

Maurizio Cimadamore maurizio.cimadamore at oracle.com
Mon Aug 19 03:31:50 PDT 2013


I think you are essentially describing a subset of what the compiler 
used to do. In fact, I believe your example used to work.
As it's been said in this thread - it's not a technical problem; we 
already have the infrastructure to do all flavours of type-checking :-)
Finding the right balance between expressiveness/complexity/ease of use 
is the challenge here.

Maurizio

On 17/08/13 00:25, Ali Ebrahimi wrote:
> Hi,
>
>
> On Fri, Aug 16, 2013 at 2:46 PM, Maurizio Cimadamore 
> <maurizio.cimadamore at oracle.com 
> <mailto:maurizio.cimadamore at oracle.com>> wrote:
>
>     On 16/08/13 00:36, Ali Ebrahimi wrote:
>
>         I think this can also happen in non-lambda world.
>         Can you show some example that how this may happen for comparing?
>
>     I don't think you even need comparing to get to that; if your
>     strategy is: type-check the lambda against each possible target
>     (hence, multiple times) and reject each target whenever the lambda
>     cannot be type-checked against that target, it means that overload
>     resolution depends on errors in the body of the lambda. Example:
>
>     interface Function<X, Y> {
>        Y m(X x)
>     }
>
>     m(Function<String, Integer> f)
>     m(Function<Integer, Integer> f)
>
> One moment I thought we are in future. I see that is typo
> .
>
>
>     m(x->x.intValue())
>
>     so, you take the lambda, try to check it using F<S, I> as a target
>     - you get an error, as there's no member called intValue() in
>     String - so you rule out the first candidate. Then you check the
>     second method, and everything is fine. So you pick the second -
>     you don't even get to most specific, as the first is not applicable.
>
>     This seems relatively straightforward - but what if the body was:
>
>     m(x-> SomeClass.g(x))
>
>     well, now the fact that the lambda can be type-checked depends on
>     the fact that we can resolve 'g' with a given parameter type;
>     which ultimately means that the 'm' method that will be picked
>     will depend on which methods are available in SomeClass. 
>
> I don't get this. First, we are in Type Inference phase and we do 
> lambda body type checking for each m's overload independently, So 
> during this process we already chosen m. So result of type checking 
> only verify the chosen m.
> if type inference process for m's overloads only find one correct 
> answer, So go ahead.
>
>     Add one more method there and you can get a different behavior (as
>     resolution for 'g' might change, as it could have i.e. a different
>     most specific)! Now, if we play devil's advocate, we can argue
>     that if some code has this statement:
>
>     SomeClass.g(someActual)
>
>     Its semantics (i.e. which method get choosen) is always going to
>     depend on the set of methods available on SomeClass. And, even if
>     we do this:
>
>     f(SomeClass.g(someActual))
>
>     we are going to get different selection of f, based on g's most
>     specific.
>
> This is my point. So this problem does not limits to lambdas. We 
> already had it.
>
>
>     But there's I think, an important distiction to be made: in the
>     lambda case you basically don't know what the type of x is. In the
>     other two cases, the type of the actual argument will be well
>     known. So, I think a lot of the discomfort over the lambda case is
>     coming from the fact that the user will have to reason about _two_
>     processes happening in parallel: the selection of the lambda
>     parameter type 'x' and the selection of the innermost overload
>     candidate 'g'.
>
> I say this again, in my proposed scheme we do type inference for each 
> overload independently. first, for x we have inferred type from 
> inference context for current overload and second, selection of the 
> innermost overload candidate 'g' does not change current in process 
> overload. we only type check lambda body for current overload to 
> infere lambda return type. Then if type checking was successful and we 
> inferred lambda's return type, we verify that with expected type based 
> on current overload.
> consider this example:
> suppose we have:
> class Person{
>   private String name;
>   private int age;
>
> //getters & setters
> ...
> }
>
> Stream<Person> persons = ...;
>
> persons.sorted(comparing(x -> x.getAge()));
>
> we have 4 overload for comparing:
> for each overload we run inference process separately.
> suppose we do this process for
> <T, U extends Comparable<? super U>> comparing(
>             Function<? super T, ? extends U> keyExtractor)
>
> in first step we try to infer comparing's type arguments from 
> inference context. As I said before for type checking lambda bodies we 
> only needs its input type arguments to be inferred. In this case we 
> only need to type args T. As you can see in this case compiler can 
> infer type Person for comparing's type args T. So we can inject this 
> type to lambda parameter and start lambda body type checking. So in 
> fact, now  we have this.
> persons.sorted(Comparator.<Person>comparing((Person x) -> x.getAge()));
> lambda body is only one expression x.getAge() as you can see x already 
> known and its type is Person and person has member getAge(), So type 
> checking is successful. But, this is not end of story, since we should 
> infer lambda return type.
>
> Suppose we are in mode 1:
> So inferred return type for lambda based on expression x.getAge() 
> would be int.
>
> After inferring (computing) return type we verify result (int) with 
> expected type based on current overload.
> The expected type is U extends Comparable<? super U>
>
> As I said before we do lambda return type verification in 3 phase or mode:
> 1) without boxing, unboxing and widening (can not do boxing, unboxing 
> and widening)
> 2) without boxing and unboxing but with widening (can not do boxing, 
> unboxing but can do widenin)
> 3) with boxing, unboxing and widening (can do boxing, unboxing and 
> widening)
>
> For mode 1 verification fails. int is not reference type and does no 
> satisfy  U extends Comparable<? super U>
> So we give up this overload in mode 1.
>
> For mode 2 verification also fails base on above reasoning in mode 1.
> So we give up this overload in mode 2.
> For mode 3 (with boxing) verification does not fail. Since Integer 
> satisfy constraint U extends Comparable<? super U>
> So we add this overload in mode 3 applicable overloads list.
>
> We do this process for 3 other overload.
>
> <T> Comparator<T> comparing(ToIntFunction<? super T> keyExtractor)
>
> Inferred T would be Person for this overload also. So lambda body type 
> checking would be
> successful.
> So let's go final step: lambda return type verification
> In this case expected type is int.
> So for mode 1 verification is fine. int == int
> we add this overload in mode 1 applicable overloads list.
>
> So for mode 2 verification is fine. int == int , does not need to widening
> we add this overload in mode 2 applicable overloads list.
> So for mode 3 verification is fine. int == int, does not need to 
> boxing, unboxing and widening
> we add this overload in mode 3 applicable overloads list.
>
> <T> Comparator<T> comparing(ToLongFunction<? super T> keyExtractor)
> In this case expected type is long.
>
> So for mode 1 verification fails. int <> long
> So we give up this overload in mode 1.
>
> for mode 2 verification is fine. ((long)int) == long, can do widening
> we add this overload in mode 2 applicable overloads list.
> for mode 3 verification is fine. ((long)int) == long, can do widening
> we add this overload in mode 3 applicable overloads list.
>
>
> Comparator<T> comparing(ToDoubleFunction<? super T> keyExtractor)
> In this case expected type is double.
>
> So for mode 1 verification fails. int <> double
> So we give up this overload in mode 1.
>
> for mode 2 verification is fine. ((double)int) == double, can do widening
> we add this overload in mode 2 applicable overloads list.
> for mode 3 verification is fine. ((double)int) == double, can do widening
> we add this overload in mode 3 applicable overloads list.
>
> Finally, we process obtained results for all overloads.
> As you can see in mode 1's applicable overloads list exists only one 
> entry. So inference is successful.
>
> if mode 1's applicable overloads list had more than one entry, then 
> inference fails and ERROR.
> if mode 1's applicable overloads list was empty check mode 2 
> applicable overloads list based on above process. After mode 2 go mode 
> 3 if mode 2's applicable overloads list was empty.
>
> So best on my proposed scheme
> <T> Comparator<T> comparing(ToIntFunction<? super T> keyExtractor)
> is best and only applicable overload.
>
>     There's an interplay between the two process that is very subtle -
>     one might argue that 90% of the times user won't even have to know
>     about that - but what about that 10% of the times? Will the user
>     have the right mental model to even reason about the problem at hand?
>
> I don't think so at list in real world code.  In fact I imagine 
> reverse would be more correct.
> Consider comparing case.
>
>
> Regards,
> Ali Ebrahimi
>



More information about the lambda-spec-observers mailing list