The TL;DR is ultimately that type inference is hard - it should be pragmatic enough that you nearly always get the type you wanted, but strict enough that you don’t loose precision. These goals have to be met, while the compiler has to be as fast as possible (faster than scalac anyway!). There are trade-offs between all of this and I, personally, really like the way Dotty infers types right now.
Both compound types and union types incur a penalty when being inferred. My hunch from reading your post is that you are referring to discussions regarding union types a.k.a “or-types”: e.g: Int | String
. Where intersection (or compound) types are A & B
which is equivalent (almost) to Scala 2’s A with B
.
So let’s talk about union types For instance, if you do:
val x = if (cond) 1 else "doge"
In Scala 2, you’ll get the inferred type Any
which is the least upper bound between the two types since (in pseudo code):
1 <: Int <: AnyVal
"doge" <: String <: AnyRef
(AnyRef | AnyVal) <: Any
Since Dotty has union types, we have to decide what to do in the if
-expression above:
- Do we infer a union i.e. the least upper bound between the two (
Any
)?
- Do we simply infer the union (
Int | String
)?
We could have gone even further with singleton types:
- Do we infer the type
1 | "doge"
?
-
1 | String
?
-
Int | "dog"
?
As you can imagine, all this comes with a lot of complexity, subtype checking becomes more expensive, the deeper the type is the longer it takes. For types which contain dis/con-junctions we have to potentially check twice as many things. It doesn’t help if they are aliased, or derived from a type member or term paths.
Another thing that is quite annoying is that most of the time, you really don’t want the most specific type, you want the compiler to automagically widen things - but not too much. Imagine you did:
case class InvariantList[A](xs: A*)
trait A
trait B
class C extends A with B
class D extends A with B
val xs = InvariantList(new B{}, new C) // xs: InvariantList[C | D]
def foo(xs: InvariantList[A & B]) = ???
foo(xs) // error: expect InvariantList[A & B], got InvariantList[B | C]
Now you see that we got an inferred type which was more precise than we wanted to, and since our list is invariant, now it doesn’t fit in the type
Sadness… but, don’t worry, Dotty does the right thing in the above example and widens the type of xs
to InvariantList[A & B]
So what do we do currently with regards to unions and intersections and singletons of the twain? Well, through several issues and user reports, we have adapted the type inference to take a more pragmatic approach. I’m not sure the discussion you’ve read are 100% up to date, we do infer union types where they are expected, but we don’t infer them all the time. We currently don’t support unions with singleton type members, this is for technical reasons, we hope to have them eventually. When it comes to automatically inferring unions, we have some kinks to work out, for instance:
def foo(x: String | Int) = println(x)
val x = if (cond) 1 else "doge"
foo(x) // error: got Any expected `Int | String`
But, when the compiler can figure out beforehand that the expected type is a union, there is no problem:
def foo(x: String | Int) = println(x)
foo { if (cond) 1 else "doge" }
It’s safe to say that unions and intersections are a useful concept - and as @cvogt said about unions, the place where they are most useful is most likely as an anonymous ADT in the case where you have two disjoint types that should be the argument to the same method:
def closeResource(cls: Closeable | MyFileReader): Try[Unit] = Try {
closeable match {
case cls: Closeable =>
cls.close()
case cls: MyFileReader =>
cls.doSomeCleanupThingy()
cls.closeFileReader()
}
}
At the call site, you’d probably never want to infer a union, you’d always have a single part of the disjunction e.g. MyFileReader
or some subclass of Closeable
, if you need to keep both, you’ll have to annotate the types. So from this use case, we’re doing what makes sense.
Currently, Dotty nearly always gets what you want and with less boilerplate than in Scala 2 to boot. I used the Dotty compiler when rewriting the Dotty REPL, and I must say - when I converted the project to Scala 2 (we had to for sbt compatibility), it felt a bit painful to lose the new features and the type inference in Scala 2 felt somewhat clumsy in comparison. Obviously, being on the compiler team, I’m extremely biased, but that’s my take on it anyway
Hope this answers your questions, if you have any more wonderings regarding type inference and our type system in Dotty, @odersky and @smarter are your top experts.
Cheers,
Felix