Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding definition and desugaring of "Option" in Scala 3 book

Tags:

scala

scala-3

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:

  1. How exactly does the mechanism of this desugaring work? In particular why does Some by default extend Option[T] whereas None extends Option[Nothing]?
  2. This seems like a strange way to define 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.

like image 632
Clinton Avatar asked Jun 08 '21 12:06

Clinton


2 Answers

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.

  • If T is invariant, we have to specify an extends clause explicitly
  • If T is covariant, we get Nothing
  • If 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)

like image 162
Silvio Mayolo Avatar answered Sep 22 '22 18:09

Silvio Mayolo


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.

like image 34
Yuval Itzchakov Avatar answered Sep 19 '22 18:09

Yuval Itzchakov