You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Prototype] Better type inference for lambdas (e.g., as used in folds)
No version of Scala has ever been able to infer the following:
val xs = List(1, 2, 3)
xs.foldLeft(Nil)((acc, x) => x :: acc)
To understand why, let's have a look at the signature of `List[A]#foldLeft`:
def foldLeft[B](z: B)(op: (B, A) => B): B
When typing the foldLeft call in the previous expression, the compiler
starts by creating an unconstrained type variable ?B, the challenge is then to
successfully type the expression and instantiate `?B := List[Int]`.
Typing the first argument is easy: `Nil` is a valid argument if we add a
constraint:
?B >: Nil.type
Typing the second argument is where we get stuck normally: we need to choose a type
for the binding `acc`, but `?B` is a type variable and not a fully-defined type,
this is solved by instantiating `?B` to one of its bound, but no matter what
bound we choose, the rest of the expression won't typecheck:
- if we instantiate `?B := Nil.type`, then the body of the lambda `x :: acc` is
not a subtype of the expected result type `?B`.
- if we instantiate `?B := Any`, then the body of the lambda does not
typecheck since there is no method `::` on `Any`.
But... what if we just let `acc` have type `?B` without instantiating it first?
This is not completely meaningless: `?B` behaves like an abstract type except
that its bounds might be refined as we typecheck code, as long as narrowing
holds (#), this should be safe.
The remaining challenge then is to type the body of the lambda `x :: acc` which
desugars to `acc.::(x)`, this won't typecheck as-is since `::` is not defined on
the upper bound of `?B`, so we need to refine this upper bound somehow, the
heuristic we use is:
1) Look for `::` in the lower bound of `?B >: Nil.type`, Nil does have such a member!
2) Find the class where this member is defined: it's `List`
3) If the class has type parameters, create one fresh type variable
for each parameter slot, the resulting type is our new upper bound,
so here we get `?B <: List[?X]` where `?X` is a fresh type variable.
We can then proceed to type the body of the lambda:
acc.::(x)
This first creates a type variable `?B2 >: ?X`, because `::` has type:
def :: [B >: A](elem: B): List[B]
Because the result type of the lambda is `?B`, we get an additional constraint:
List[?B2] <: ?B
We know that `?B <: List[?X]` so this means that `?B2 <: ?X`, but we
also know that `B2 >: ?X`, so we can instantiate `?B2 := ?X` and `?B := List[?X]`.
Finally, because `x` has type Int we have `?B2 >: Int` which simplifies to:
?X >: Int
Therefore, the result type of the foldLeft is `List[?X]` where `?X >: Int`,
because `List` is covariant, we instantiate `?X := Int` to get the most precise
result type `List[Int]`.
Note that the the use of fresh type variables in 3) was crucial here: if we had
instead used wildcards and added an upper bound `?B <: List[_]`, then we would
have been able to type `acc.::(x)`, but the result would have type `List[Any]`,
meaning the result of the foldLeft call would be `List[Any]` when we wanted
`List[Int]`.
\# Status
All the compiler tests pass, including bootstrapping, but one of third of the
community build breaks currently.
Even if this PR never makes it in, it has been very useful for stress-testing
our constraint solver and lead to several PRs I made over the past few days:
of this PR that would be worth getting in by themselves.
\# Open questions
- Is this actually sound?
- Are there other compelling examples where this useful, besides folds?
- Is the performance impact of this stuff acceptable?
- How do we deal with overloads?
- How do we deal with overrides?
- How does this interact with implicit conversions?
- How does this interact with implicit search in general, we might find one
implicit at a given point, but then as we add more constraints to the same
type variable, the same implicit search could find a different result. How big
of a problem is that?
(#): narrowing in fact does not hold when `@uncheckedVariance` is used, which is
why we special-case it in `typedSelect` in this commit.
0 commit comments