-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Type inference for union types is surprising #10108
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
Comments
IMO the best strategy we've thought about in the past years is the widen-if-synthesized strategy: at the points where we currently widen stuff, widen only compiler-synthesized union types. If a union type was written down by the developer, it is not synthesized, and does not get widened. Whether a union is user-written or synthesized is stored in the In the example snippet above, all the union types come from a At the last meeting we discussed another strategy, which is never to widen union types, and instead joining the types of the branches of |
Here are some alternatives to the current behavior:
If we go with (3), one tricky question is what to do with combinations of hard and soft unions. E.g. assuming
? The most obvious answer is
So, if we do not want to treat
I am assuming here that all types that get pushed out of the join are again combined with a lub, but one that yields a hard union, not a soft one. At first, treating unions asymmetrically looks weird. But consider that these are all declared types, so programmerts have fine-grained control what behavior they intend. For instance, one could force a union type to be never widened by declaring it |
@sjrd Our comments were written in parallel, but it seems they come to similar conclusions. |
For unions with Null, I see two possibilities:
An analogue of
|
Scala's eager widening also hurts singleton values. Is there a common solution to both (singleton widening / union widening) problems or are they mutually exclusive? One thing I can see is that since union types are new, there is no harm in creating special rules for them, but a common solution with singleton types still needs to take into account backward compatibility or minimize unexpected change in behavior. |
@olhotak This seems connected to the concept of super traits (see: #9028). We should never widen to a super trait (which includes the traits like Dotty already avoids widening to super traits: when it sees something like It would be great if I think in the most common cases, the typing of if-then-else and pattern-matching should not be affected, as branches usually have types like |
I fear not widening to |
It turned out that it's generally clear whether an OrType should be soft or hard, so this is a good first step to solve the problem.
For Null, a big part of the problem is that there is no "non-super trait" expressing that something is nullable. The only supertype of The question comes down to what we want the canonical notation to be: if we want people to generally write Then, I wonder if this could generalize to other union types. Union types would be soft by default, but type aliases of union types would be hard (maybe this is already the existing behaviour; maybe it removes the need for hard unions):
But then the recommended style must be to name your union types. |
I would suggest to special-case I'm afraid of the potential consequences of making unions asymmetric. The suggested |
Hmm, would there possibly be an option to skip widening unions entirely, e.g. using a
I agree, and would like to add that there's perfectly acceptable code that works with fully inferred unions - and that may even work better, with less type annotations required. Having an option to disable widening would allow the community to experiment and refine the libraries and best practices to support and benefit from inferred union types. |
It turned out that it's generally clear whether an OrType should be soft or hard, so this is a good first step to solve the problem.
Fix #10108: Distinguish between soft and hard union types
@neko-kai I think enabling changed inference behavior for union types under a flag would require a lot more experimentation. Without knowing whether the new behavior would be at least half-way reasonable, we should not add mode-switches like that to the compiler. So to get anywhere someone would have to fork the compiler, do the experiments, and, fi successful, convince us that it's worth it. But I can't see it getting in if it does not at least compile the standard library without errors. |
I agree strongly that it's worth standing firm on mode-switches, as per scala/scala-dev#430 |
The major difficulty you'll run into is the interaction with implicit search: before implicit search, we instantiate all previously constrained type variables referred to in the type we're doing a search on (if we don't, we easily get too many results and thus ambiguous implicits), if we don't widen unions at that point, then we might end up doing a search for |
@smarter trait Monad[-F[_], +F1[_]] {
extension [A, B](fa: F[A]) def flatMap(afb: A => F[B]): F1[B]
extension [A](a: A) def pure: F1[A]
} See example https://scastie.scala-lang.org/YIpUUo6HRD6GiWH9CahQEA Note that if you remove variance from the example, the syntax would still work, but not the explicit summon for the union type. I guess that's because unions are widened when resolving extension methods. |
Minimized example
Output
The problem is that a union type is widened at each inference step. Hence the declared type
Int | String
ofx
gets lost in the type ofy
.This widening can lead to mysterious behavior in the type checker. Here's an example:
The first
corresponds
tests succeeds, but if we eta-expand the argument toconsistent(_, _)
it fails.Expectation
I hope we can come up with a more predictable type inference algorithm.
The text was updated successfully, but these errors were encountered: