I'm starting a Scala role in a few weeks yet I haven't written any Scala before (yes, my future employers know this), although I've written a lot of C# and Haskell. Anyway I was skimming through the Scala 3 book, and found this example:
enum Option[+T]:
case Some(x: T)
case None
Which apparently dusugars into:
enum Option[+T]:
case Some(x: T) extends Option[T]
case None extends Option[Nothing]
My two questions are:
Some
by default extend Option[T]
whereas None
extends Option[Nothing]
?Option
. I would define it like this:enum Option[+T]:
case Some(x: T) extends Option[T]
case None extends Option[T]
Indeed with None
extending Option[None]
wouldn't this fail?
Option[string] x = None;
As Option[T]
is covariant in T
and None
is not a subtype of string
?
I'm missing something quite fundamental here I'm sure.
An excellent source for how desugaring of enums works is the original proposal at Issue #1970.
The relevant section for your question on that page is titled "Desugarings". Let's take a look at your Option
definition.
enum Option[+T]:
case Some(x: T)
case None
Some
is a parameterized case under an enum with type parameters, so Rule #4 applies
If E is an enum class with type parameters Ts, then a case in its companion object without an extends clause
case C <params> <body>
...
For the case where C does not have type parameters, assume E's type parameters are
V1 T1 > L1 <: U1 , ... , Vn Tn >: Ln <: Un (n > 0)
where each of the variances Vi is either '+' or '-'. Then the case expands to
case C <params> extends E[B1, ..., Bn] <body>
So your Some
case gets an extends
clause for Option[T]
, as you'd expect. Then Rule #5 gets us our actual case class.
On the other hand, your None
, by the same token, uses no type parameters, so the result is based on variance.
T
is invariant, we have to specify an extends
clause explicitlyT
is covariant, we get Nothing
T
is contravariant, we get Any
Your T
is covariant, so we extend Option[Nothing]
, which remember is a subtype of Option[S]
for any S
. Hence, your assignment (and remember, in Scala, we use val
/ var
to declare variables, not just the type name)
val x: Option[String] = None;
None
is a value of type None.type
, which extends Option[Nothing]
. Option[Nothing]
is a subtype of Option[String]
since Nothing
is a subtype of String
. So the assignment succeeds.
This is the same reason Nil
(the empty list) extends List[Nothing]
. I can construct a Nil
which has a concrete type (a subtype of List[Nothing]
), and then later I can come along and prepend whatever I want to it. The resulting list is no longer a List[Nothing]
; 1 +: Nil
is a List[Int]
and "a" +: Nil
is a List[String]
, but the point is, that Nil
can come from anywhere in my program and we don't have to decide what type we want it to be up-front. We can't (safely) use covariance on a mutable data type, so this all only works because List
and Option
are immutable. A mutable collection like ArrayBuffer
is invariant in its type parameter. (Note: Java's built-in arrays are covariant, and that's unsound and causes all kinds of problems)