Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Better typing for overloaded higher-order methods #6871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 15, 2018

Conversation

adriaanm
Copy link
Contributor

@adriaanm adriaanm commented Jun 29, 2018

TODO:

  • spec update
  • performance
  • clean up OverloadedArgFunProto

For better typing for an overloaded higher-order method such as map (prevalent in the new collections
/ spark apis), regardless of whether the actual argument is a function literal, a method reference that
will be eta-expanded (based on the expected type), or a method value (m _).

Before, we triggered this based on the syntactic shape of the arguments, but that results (in our case)
in difference between foo, foo _ and x => foo(x) as arguments to overloaded method.

The first commit (ProtoType generalizes [Bounded]WildcardType) sneakily sets the scene, pretty
specifically for the second one, but I do expect it to be useful for other cases.

Normally, overload resolution types the arguments to the alternatives without an expected type.
However, typing function literals and eta-expansion are driven by the expected type:

  • function literals usually don't have parameter types, which are derived from the expected type;

  • eta-expansion right now only happens when a function/sam type is expected.

(Dotty side-steps these issues by eta-expanding regardless of expected type.)

Now that the collections are full of overloaded HO methods, we should try harder to type check them
nicely.

To avoid breaking existing code, we only provide an expected type (for each argument position) when:

  • there is at least one FunctionN type expected by one of the overloads: in this case, the expected
    type is a FunctionN[Ti, ?], where Ti are the argument types (they must all be =:=), and the expected
    result type is elided using a wildcard. This does not exclude any overloads that expect a SAM,
    because they conform to a function type through SAM conversion

  • OR: all overloads expect a SAM type of the same class, but with potentially varying result types
    (argument types must be =:=)

We allow polymorphic cases, as long as the types parameters are instantiated by the AntiPolyType prefix.

In all other cases, the old behavior is maintained: Wildcard is expected.

(Slightly) more formally:

Consider an overloaded method m_i, with N overloads i = 1..N, and an expected argument type at
index j, a_ij:

def m_1(... a_1j, ...)
..
def m_N(... a_Nj, ...)

Any polymorphic method m_i will be reduced to the monomorphic case by pushing down the method's
PolyType to its arguments a_ij.

The expected type for the argument at index j will be more precise than the usual WildcardType
(?), if all types a_1j..a_Nj are function-ish types that denote the same parameter types p1..pM.

A "function-ish" type is a FunctionN[p1,...,pM] (or PartialFunction), or the equivalent SAM type.
(We first unwrap any PolyTypes.)

The non-wildcard expected type will be

  • PartialFunction[p1, ?], if an a_ij expects a partial function;
  • else, if there is a subclass of FunctionM among the a_ij, it is FunctionM[p1, pM, ?];
  • else, if all a_ij are of the same SAM type (allowing for varying result types),
    that SAM type (with ? result type).

In each case, any type parameter not already resolved by overloading, or the outer context, it
approximated by ?.

PS: type equivalence is decided as tp1 <:< tp2 && tp2 <:< tp1, and not tp1 =:= tp2 (the latter is
actually stricter).

@adriaanm adriaanm added the WIP label Jun 29, 2018
@scala-jenkins scala-jenkins added this to the 2.13.0-M5 milestone Jun 29, 2018
@adriaanm

This comment has been minimized.

@@ -2973,7 +3091,7 @@ trait Types
override def mapOver(map: TypeMap): Type = {
val pre1 = if (pre.isInstanceOf[ClassInfoType]) pre else map(pre)
if (pre1 eq pre) this
else OverloadedType(pre1, alternatives)
else OverloadedType(pre1, alternatives) // TODO: shouldn't we also map over the alternatives?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's tricky, because we can't just go an clone alternatives here, as these symbols are serving double duty of carrying an info and linking to an actual member.

For the particular case of AsSeenFrom, we take care to later call memberType on the eventually selected alternative.

@adriaanm adriaanm force-pushed the overload_proto branch 3 times, most recently from 8114db5 to f9a82f9 Compare July 9, 2018 12:52
@adriaanm adriaanm changed the title Overload resolution propagates expected argument types more [ci: last-only] More precise prototype for args of overloaded method Jul 9, 2018
@adriaanm
Copy link
Contributor Author

@SethTisue could you run a community build on this one?

@SethTisue
Copy link
Member

@SethTisue
Copy link
Member

specs2 fails with:

[specs2] [error] /home/jenkins/workspace/scala-2.13.x-integrate-community-build/target-0.9.12/project-builds/specs2-41304f739191bc5c34e6c3a1a87816dcae5ea3c3/common/shared/src/main/scala/org/specs2/data/Trees.scala:48:29: type mismatch;
[specs2] [error]  found   : Option[A(in method clean)] => Option[A(in method clean)]
[specs2] [error]  required: ?<: org.specs2.fp.Tree[?] => ?
[specs2] [error]     prune(t, (a: Option[A]) => a).getOrElse(Leaf(initial))
[specs2] [error]                             ^
[specs2] [error] /home/jenkins/workspace/scala-2.13.x-integrate-community-build/target-0.9.12/project-builds/specs2-41304f739191bc5c34e6c3a1a87816dcae5ea3c3/common/shared/src/main/scala/org/specs2/data/Trees.scala:48:29: type mismatch;
[specs2] [error]  found   : Option[A(in method clean)] => Option[A(in method clean)]
[specs2] [error]  required: ?<: org.specs2.fp.Tree[?] => ?
[specs2] [error]     prune(t, (a: Option[A]) => a).getOrElse(Leaf(initial))
[specs2] [error]                             ^
[specs2] [error] /home/jenkins/workspace/scala-2.13.x-integrate-community-build/target-0.9.12/project-builds/specs2-41304f739191bc5c34e6c3a1a87816dcae5ea3c3/common/shared/src/main/scala/org/specs2/data/Trees.scala:54:49: type mismatch;
[specs2] [error]  found   : A(in method prune) => Option[(some other)B(in method prune)]
[specs2] [error]  required: ?<: org.specs2.fp.Tree[?] => ?
[specs2] [error]     val tbs = t.subForest.flatMap(t => prune(t, f))
[specs2] [error]                                                 ^
[specs2] [error] /home/jenkins/workspace/scala-2.13.x-integrate-community-build/target-0.9.12/project-builds/specs2-41304f739191bc5c34e6c3a1a87816dcae5ea3c3/common/shared/src/main/scala/org/specs2/data/Trees.scala:54:49: type mismatch;
[specs2] [error]  found   : A(in method prune) => Option[(some other)B(in method prune)]
[specs2] [error]  required: ?<: org.specs2.fp.Tree[?] => ?
[specs2] [error]     val tbs = t.subForest.flatMap(t => prune(t, f))
[specs2] [error]                                                 ^

@retronym
Copy link
Member

@niktrop heads up: we're generalizing type inference in 2.13 to allow overloading of methods like Map.exists. We'd appreciate if you could watch this ticket and see if the spec changes and tests here are something you could use as a basis of updating the IntelliJ typechecker.

@niktrop
Copy link

niktrop commented Jul 17, 2018

@retronym Thanks! I'll have a look.

@adriaanm adriaanm force-pushed the overload_proto branch 2 times, most recently from 5a37ea2 to 22a1ec3 Compare July 18, 2018 11:01
@adriaanm
Copy link
Contributor Author

adriaanm commented Jul 18, 2018

Another shot at community building: https://scala-ci.typesafe.com/job/scala-2.13.x-integrate-community-build/1249/

@adriaanm
Copy link
Contributor Author

Next failure is in gigahorse:

okhttp/src/main/scala/gigahorse/support/okhttp/OkhClient.scala:51: type mismatch;
 found   : gigahorse.support.okhttp.OkHandler.FullOkHandler[gigahorse.FullResponse]
 required: ?<: gigahorse.FullResponse => ?
    processFull(request, OkHandler[FullResponse](identity))
                                                ^
okhttp/src/main/scala/gigahorse/support/okhttp/OkhClient.scala:55: type mismatch;
 found   : gigahorse.support.okhttp.OkHandler.FullOkHandler[A(in method run)]
 required: ?<: gigahorse.FullResponse => ?
    processFull(request, OkHandler[A](f))
                                     ^
okhttp/src/main/scala/gigahorse/support/okhttp/OkhClient.scala:147: type mismatch;
 found   : gigahorse.support.okhttp.FunctionHandler[gigahorse.FullResponse]
 required: ?<: gigahorse.FullResponse => ?
    processFull(request, FunctionHandler[FullResponse](identity))
                                                      ^
okhttp/src/main/scala/gigahorse/support/okhttp/OkhClient.scala:151: type mismatch;
 found   : gigahorse.support.okhttp.FunctionHandler[A(in method processFull)]
 required: ?<: gigahorse.FullResponse => ?
    processFull(request, FunctionHandler[A](f))
                                           ^

@SethTisue could you double check to see if there's another new failure? scodec-bits had a test fail, while it was passing in the last community build that I compared to -- for now assuming that's not related to this PR

@SethTisue
Copy link
Member

SethTisue commented Jul 18, 2018

I've seen the scodec-bits test failure in other runs, no worries there.

this PR seems to have made shapeless green when it was red before, was that on purpose? /cc @milessabin

(shapeless being green adds two failures, case-app and play-json, but I checked and the errors are similar to those from older runs before shapeless regressed, so no worries there)

(also note that as of run 1250 we have moved to a newer SHA, fd820e4. so if you end up wanting to do another community build run, I'd suggest rebasing this PR onto fd820e4 first.)

@adriaanm
Copy link
Contributor Author

adriaanm commented Jul 19, 2018 via email

@adriaanm
Copy link
Contributor Author

The gigahorse fail boils down to:

abstract class Sam[A] { def apply(a: String): A }

class Test {
  def map[A](f: String => A): A = map(new Sam[A] { def apply(a: String): A = f(a) })
  def map[A](f: Sam[A]): A = ???
}

🤔 we should probably support that 😄

@adriaanm adriaanm changed the title More precise prototype for args of overloaded method More precise prototype for args of overloaded method [ci: last-only] Jul 20, 2018
@adriaanm
Copy link
Contributor Author

Note to self (so I remember after vacation :-)): I have some further non-essential WIP to also make TypeVars prototypes at overload_proto_wip.

@adriaanm
Copy link
Contributor Author

@smarter
Copy link
Member

smarter commented Jul 20, 2018

@adriaanm It'd be nice if we could establish a list of all the test cases related to the overloading resolution changes in 2.13 so that we can try to align dotty with it.

@adriaanm
Copy link
Contributor Author

adriaanm commented Jul 20, 2018 via email

@milessabin
Copy link
Contributor

shapeless was failing before because of a change in the constructor signature of ImplicitSearch in the byname implicits PR ... I think it's highly unlikely that anything happening here could have changed that.

@adriaanm
Copy link
Contributor Author

adriaanm commented Aug 2, 2018

re-review by @retronym?

@SethTisue
Copy link
Member

https://scala-ci.typesafe.com/job/scala-2.13.x-integrate-community-build/1253/

the results here were the same as run 1254, another 2.13.x run from around the same time, so I'd say we have a clean bill of health.

@retronym
Copy link
Member

retronym commented Aug 6, 2018

I've run the performance tests for the scala corpus, too (after editing the sources to avoid what appears to be a reasonable new cyclic error after this PR).

It also sees the ~2% slowdown. Not sure yet how much of that is inherent to the change vs something we can ratchet down (e.g. by caching whether a given class symbol is a functional interface or not)

@SethTisue SethTisue modified the milestones: 2.13.0-M5, 2.13.0-RC1 Aug 7, 2018
@adriaanm adriaanm modified the milestones: 2.13.0-RC1, 2.13.0-M5 Aug 7, 2018
@adriaanm adriaanm self-assigned this Aug 7, 2018
@adriaanm
Copy link
Contributor Author

adriaanm commented Aug 8, 2018

@retronym are you comfortable with merging this for M5 and working on performance for RC1?

Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtle stuff, but LGTM without going through all the details.

@@ -629,7 +631,7 @@ trait Implicits {
private def matchesPtView(tp: Type, ptarg: Type, ptres: Type, undet: List[Symbol]): Boolean = tp match {
case MethodType(p :: _, restpe) if p.isImplicit => matchesPtView(restpe, ptarg, ptres, undet)
case MethodType(p :: Nil, restpe) => matchesArgRes(p.tpe, restpe, ptarg, ptres, undet)
case ExistentialType(_, qtpe) => matchesPtView(normalize(qtpe), ptarg, ptres, undet)
case ExistentialType(_, qtpe) => matchesPtView(methodToExpressionTp(qtpe), ptarg, ptres, undet)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

* - there is at least one FunctionN type expected by one of the overloads:
* in this case, the expected type is a FunctionN[Ti, ?], where Ti are the argument types (they must all be =:=),
* and the expected result type is elided using a wildcard.
* This does not exclude any overloads that expect a SAM, because they conform to a function type through SAM conversion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm.. i though sam conversion works on literals, i.e., a function literal can get a sam type. if the expected type is a Function, isn't the literal typed with a Function type? how can that get converted to a sam type later?

anyway, it works as intended, so ignore my rambling.

scala> trait S[-T, +U] { def apply(x: T): U }
defined trait S

scala> object C { def m(f: String S Int) = 0; def m(f: String => String) = 1 }
defined object C

scala> C.m(x => 1)
res5: Int = 0

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is first assigned the Function type, but the expected type is still the SAM. That gap is closed during adapt, case (14).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha! thanks!

This will serve as a hook into type checking to represent
expectation of types that cannot (cheaply) be encoded as
existing types.
Normally, overload resolution types the arguments to the alternatives
without an expected type. However, typing function literals and
eta-expansion are driven by the expected type:

  - function literals usually don't have parameter types, which are
    derived from the expected type;

  - eta-expansion right now only happens when a function/sam type is
    expected.

(Dotty side-steps these issues by eta-expanding regardless of
expected type.)

Now that the collections are full of overloaded HO methods, we should
try harder to type check them nicely.

To avoid breaking existing code, we only provide an expected type (for
each argument position) when:

 - there is at least one FunctionN type expected by one of the
   overloads: in this case, the expected type is a FunctionN[Ti, ?],
   where Ti are the argument types (they must all be =:=), and the
   expected result type is elided using a wildcard. This does not
   exclude any overloads that expect a SAM, because they conform to a
   function type through SAM conversion

 - OR: all overloads expect a SAM type of the same class, but with
   potentially varying result types (argument types must be =:=)

We allow polymorphic cases, as long as the types parameters are
instantiated by the AntiPolyType prefix.

In all other cases, the old behavior is maintained: Wildcard is
expected.

(Slightly) more formally:

Consider an overloaded method `m_i`, with `N` overloads `i = 1..N`,
and an expected argument type at index `j`, `a_ij`:

```
def m_1(... a_1j, ...)
..
def m_N(... a_Nj, ...)
```

Any polymorphic method `m_i` will be reduced to the monomorphic case
by pushing down the method's `PolyType` to its arguments `a_ij`.

The expected type for the argument at index `j` will be more
precise than the usual `WildcardType` (`?`), if all types `a_1j..a_Nj`
are function-ish types that denote the same parameter types `p1..pM`.

A "function-ish" type is a `FunctionN[p1,...,pM]` (or
`PartialFunction`), or the equivalent SAM type. (We first unwrap any
PolyTypes.)

The non-wildcard expected type will be
  - `PartialFunction[p1, ?]`, if an `a_ij` expects a partial function;
  - else, if there is a subclass of `FunctionM` among the `a_ij`,
    it is `FunctionM[p1, pM, ?]`;
  - else, if all `a_ij` are of the same SAM type (allowing
    for varying result types), that SAM type (with `?` result type).

In each case, any type parameter not already resolved by overloading,
or the outer context, it approximated by `?`.

PS: type equivalence is decided as `tp1 <:< tp2 && tp2 <:< tp1`,
and not `tp1 =:= tp2` (the latter is actually stricter).
@adriaanm

This comment has been minimized.

@adriaanm

This comment has been minimized.

@adriaanm adriaanm changed the title More precise prototype for args of overloaded method [ci: last-only] More precise prototype for args of overloaded method Aug 10, 2018
Propagate more type information to improve eta-expansion
and function parameter type inference for polymorphic,
higher-order overloaded methods.
@adriaanm
Copy link
Contributor Author

I think this is good to go, assuming we are ok with working on regaining the 2% performance drop by RC1.

@retronym
Copy link
Member

retronym commented Aug 11, 2018

Let me take a look at some profiles to see how likely it is we can reclaim the performance.

@retronym
Copy link
Member

retronym commented Aug 11, 2018

Some initial ideas:

Implement OverloadedArgFunProto.isHigherKinded, that's could just be constant false, right?

I see some stack traces like:

   scala.collection.immutable.List scala.reflect.internal.Definitions$DefinitionsClass.functionOrPfOrSamArgTypes(scala.reflect.internal.Types$Type) line: 751	1
   boolean scala.reflect.internal.Types$OverloadedArgFunProto.sameHOArgTypes$1(scala.reflect.internal.Types$Type, scala.reflect.internal.Types$Type) line: 1294	1
   scala.Tuple3 scala.reflect.internal.Types$OverloadedArgFunProto.$anonfun$functionArgsProto$2(scala.reflect.internal.Types$OverloadedArgFunProto, scala.Tuple3, scala.Tuple3, scala.reflect.internal.Types$Type) line: 1303	1
   java.lang.Object scala.reflect.internal.Types$OverloadedArgFunProto$$Lambda$394.26866603.apply(java.lang.Object, java.lang.Object) line: -1	1
   java.lang.Object scala.collection.LinearSeqOps.foldLeft(java.lang.Object, scala.Function2) line: 126	1
   java.lang.Object scala.collection.LinearSeqOps.foldLeft$(scala.collection.LinearSeqOps, java.lang.Object, scala.Function2) line: 122	1
   scala.reflect.internal.Types$Type scala.reflect.internal.Types$OverloadedArgFunProto.functionArgsProto$lzycompute() line: 1301	1
   scala.reflect.internal.Types$Type scala.reflect.internal.Types$OverloadedArgFunProto.functionArgsProto() line: 1289	1
   scala.reflect.internal.Types$Type scala.reflect.internal.Types$OverloadedArgFunProto.underlying() line: 1211	1
   boolean scala.reflect.internal.Types$SimpleTypeProxy.isHigherKinded() line: 138	1
   boolean scala.reflect.internal.Types$SimpleTypeProxy.isHigherKinded$(scala.reflect.internal.Types$SimpleTypeProxy) line: 138	1
   boolean scala.reflect.internal.Types$OverloadedArgFunProto.isHigherKinded() line: 1207	1
   boolean scala.reflect.internal.tpe.TypeComparers.isSubType2(scala.reflect.internal.Types$Type, scala.reflect.internal.Types$Type, int) line: 424	1

Debug to that point and then figure out what's going on. Is that a cyclic error that's getting caught higher up?

Let's make samOf short circuit the FindMember walk, rather than computing a member list and then seeing if it has exactly one element.

        val deferredMembers = (
          tp.membersBasedOnFlags(excludedFlags = BridgeAndPrivateFlags, requiredFlags = METHOD).toList.filter(
            mem => mem.isDeferred && !isUniversalMember(mem)
          ) // TODO: test
        )

        // if there is only one, it's monomorphic and has a single argument list
        if (deferredMembers.lengthCompare(1) == 0 &&
            deferredMembers.head.typeParams.isEmpty &&
            deferredMembers.head.info.paramSectionCount == 1)
          deferredMembers.head

Copy link
Member

@retronym retronym left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to work on the small performance regression performance together after M5. The change itself LGTM.

@SethTisue SethTisue merged commit dc715ce into scala:2.13.x Aug 15, 2018
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Aug 15, 2018
@SethTisue SethTisue changed the title More precise prototype for args of overloaded method Better typing for overloaded higher-order methods Aug 22, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants