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)
How exactly does the mechanism of this desugaring work? In particular why does Some by default extend Option[T] whereas None extends Option[Nothing]?
Some
by default extends Option[T]
as it has a type parameter as an argument (x: T)
and is treated similarly (but not exactly the same) as a case class, while None
is treated as a "regular" Enum value as it does not have a parameter list definition.
We can further analyze the typer compilation phase by adding -Xprint:typer
:
sealed abstract class MyOption[T >: Nothing <: Any]()
extends
Object(), scala.reflect.Enum {
+T
import MyOption.{MySome, None}
}
final lazy module val MyOption: MyOption$ = new MyOption$()
final module class MyOption$() extends AnyRef() { this: MyOption.type =>
final case class MySome[T](x: T) extends MyOption[MySome.this.T]() {
+T
val x: T
def copy[T](x: T): MyOption.MySome[T] = new MyOption.MySome[T](x)
def copy$default$1[T]: T = MySome.this.x
def ordinal: Int = 0
def _1: T = this.x
}
final lazy module val MySome: MyOption.MySome$ = new MyOption.MySome$()
final module class MySome$() extends AnyRef() {
this: MyOption.MySome.type =>
def apply[T](x: T): MyOption.MySome[T] = new MyOption.MySome[T](x)
def unapply[T](x$1: MyOption.MySome[T]): MyOption.MySome[T] = x$1
override def toString: String = "MySome"
}
case <static> val None: MyOption[Nothing] =
{
final class $anon() extends MyOption[Nothing](), scala.runtime.EnumValue {
def ordinal: Int = 1
}
new $anon(): MyOption[Nothing] & runtime.EnumValue
}
We can see MySome
is expanded into a case class
, while None
is expanded into a value defined on MyOption
.
Silvio provided the "specification" for enum
definitions (as of today (08/06/2021) I can't find a formal one), where rules 4 and 7 are what we're looking for:
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>
And for the None
case:
case C of an enum class E that does not take type parameters expands to
val C = $new(n, "C")
Here, $new is a private method that creates an instance of of E (see below).
This seems like a strange way to define Option.
Not really, it makes sense when you think about it. None
is treated a singleton in this case, and since T
is covariant in it's argument type, and Nothing
is a bottom type which means it inherits all other types in Scala, it works for any assignment.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With