Skip to content
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

Improve type inference for functions like fold #18780

Merged
merged 4 commits into from
Nov 14, 2023

Conversation

odersky
Copy link
Contributor

@odersky odersky commented Oct 28, 2023

When calling a fold with an accumulator like Nil or List() one used to have add an explicit type ascription. This is now no longer necessary. When instantiating type variables that occur invariantly in the expected type of a lambda, we now replace covariant occurrences of Nothing in the (possibly widened) instance type of the type variable with fresh type variables.

In the case of fold, the accumulator determines the instance type of a type variable that appears both in the parameter list and in the result type of the closure, which makes it invariant. So the accumulator type is improved in the way described above.

The idea is that a fresh type variable in such places is always better than Nothing. For module values such as Nil we widen to List[<fresh var>]. This does possibly cause a new type error if the fold really wanted a Nil instance. But that case seems very rare, so it looks like a good bet in general to do the widening.

@odersky odersky requested a review from smarter October 29, 2023 14:43
@odersky odersky self-assigned this Oct 29, 2023
@odersky
Copy link
Contributor Author

odersky commented Oct 29, 2023

@smarter I think this is similar to what you once proposed. I first tried a different approach, suggested by Max Heiber. He wrote a type inferencer in Scala at Meta that is used on their complete Erlang codebase. He solved the fold problem by re-typechecking with improved info after a failure. This approach is potentially more general, since it could also fix problems where we first guess a type that is too small, but is not related to Nothing. I tried that for a while, but found it very hard to pull out actionable info from the type errors. Also, the retyping approach has the potential downside that it can become inefficient, in particular with nested retypings.

So the current approach fixes the most annoying shortcomings with low complexity and no retyping.

@odersky odersky assigned smarter and unassigned odersky Oct 29, 2023
Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

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

This looks like a good approach. The proposal I had at #9076 was probably too ambitious and broke things whereas this PR is more targeted and easier to reason about.

Comment on lines 4898 to 4899
assert(!myInst.exists, i"$origin is already instantiated to $myInst but we attempted to instantiate it to $tp")
typr.println(i"instantiating $this with $tp")
if !myInst.exists then
typr.println(i"instantiating $this with $tp")
Copy link
Member

Choose a reason for hiding this comment

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

Is removing the assert actually needed? The code doesn't seem to rely on instantiating the same variable multiple times (and if it did it would likely be fragile since myInst may or may not be set depending on the typerstate).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In fact no, that was left in from an earlier attempt where the assertion triggered. I re-instantiated it.

@smarter smarter assigned odersky and unassigned smarter Nov 8, 2023
@smarter smarter added the release-notes Should be mentioned in the release notes label Nov 9, 2023
@odersky odersky assigned smarter and unassigned odersky Nov 9, 2023
Copy link
Member

@smarter smarter left a comment

Choose a reason for hiding this comment

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

Otherwise LGTM.

@@ -4916,11 +4919,7 @@ object Types {
* is also a singleton type.
*/
def instantiate(fromBelow: Boolean)(using Context): Type =
val tp = TypeComparer.instanceType(origin, fromBelow, widenUnions, nestingLevel)
if myInst.exists then // The line above might have triggered instantiation of the current type variable
Copy link
Member

Choose a reason for hiding this comment

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

This check used to be necessary so I would prefer to keep it now that the assert in instantiateWith has been added back, unless we have some strong reason to believe it no longer is.

@smarter smarter assigned odersky and unassigned smarter Nov 13, 2023
When calling a fold with an accumulator like `Nil` or `List()` one used to have add
an explicit type ascription. This is now no longer necessary. When instantiating
type variables that occur invariantly in the expected type of a lambda, we now replace
covariant occurrences of `Nothing` in the (possibly widened) type of the accumulator
with fresh type variables.

The idea is that a fresh type variable in such places is always better than Nothing. For
module values such as `Nil` we widen to `List[<fresh var>]`. This does possibly cause a new
type error if the fold really wanted a `Nil` instance. But that case seems very rare,
so it looks like a good bet in general to do the widening.
instantiateWith(typeToInstantiateWith(fromBelow))
val tp = typeToInstantiateWith(fromBelow)
if myInst.exists then // The line above might have triggered instantiation of the current type variable
Member
Copy link
Member

Choose a reason for hiding this comment

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

stray line?

@odersky odersky merged commit 563fab9 into scala:main Nov 14, 2023
18 checks passed
@odersky odersky deleted the foldl-inf branch November 14, 2023 09:08
@Kordyjan Kordyjan added this to the 3.4.0 milestone Dec 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes Should be mentioned in the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants