理解 Scala 3 书中“选项”的定义和脱糖

问题描述

几周后我将开始担任 Scala 角色,但我之前没有写过任何 Scala(是的,我未来的雇主知道这一点),尽管我已经写了很多 C# 和 Haskell。无论如何,我正在浏览 Scala 3 书,并找到了这本example

enum Option[+T]:
  case Some(x: T)
  case None

这显然是dusugars:

enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[nothing]

我的两个问题是:

  1. 这种脱糖机制究竟是如何工作的?特别是为什么 Some 认扩展 Option[T]None 扩展 Option[nothing]
  2. 这似乎是一种定义 Option 的奇怪方式。我会这样定义它:
enum Option[+T]:
  case Some(x: T) extends Option[T]
  case None       extends Option[T]

确实使用 None 扩展 Option[None] 这不会失败吗?

Option[string] x = None;

因为 Option[T]T 中是协变的,而 None 不是 string 的子类型吗?

我确定我在这里遗漏了一些非常基本的东西。

解决方法

Issue #1970 上的原始提案是关于枚举脱糖工作原理的一个很好的来源。

该页面上与您的问题相关的部分标题为“脱糖”。让我们来看看您的 Option 定义。

enum Option[+T]:
  case Some(x: T)
  case None

Some 是具有类型参数的枚举下的参数化情况,因此适用规则 #4

如果 E 是一个带有类型参数 Ts 的枚举类,那么它的伴生对象中有一个 case,没有 extends 子句

case C <params> <body>

...

对于C没有类型参数的情况,假设E的类型参数是

V1 T1 > L1 <: U1,...,Vn Tn >: Ln <: Un      (n > 0)

其中每个方差 Vi 是“+”或“-”。然后案例扩展到

case C <params> extends E[B1,Bn] <body>

因此,您的 Some 案例获得了 extendsOption[T] 子句,正如您所期望的那样。然后规则 #5 为我们提供了我们的实际案例类。

另一方面,出于同样的原因,您的 None 使用没有类型参数,因此结果基于差异。

  • 如果 T 是不变的,我们必须明确指定一个 extends 子句
  • 如果 T 是协变的,我们得到 Nothing
  • 如果 T 是逆变的,我们得到 Any

您的 T 是协变的,因此我们扩展了 Option[Nothing],记住它是 any Option[S]S 子类型。因此,您的赋值(请记住,在 Scala 中,我们使用 val / var 来声明变量,而不仅仅是类型名称)

val x: Option[String] = None;

NoneNone.type 类型的值,它扩展了 Option[Nothing]Option[Nothing]Option[String] 的子类型,因为 NothingString 的子类型。这样赋值就成功了。

这与 Nil(空列表)扩展 List[Nothing] 的原因相同。我可以构造一个具有具体类型(Nil 的子类型)的 List[Nothing],然后我可以出现并添加我想要的任何内容。结果列表不再是 List[Nothing]1 +: NilList[Int]"a" +: NilList[String],但重点是,Nil 可以来自我程序中的任何地方,而我们没有决定我们希望它是什么类型的前期。我们不能(安全地)在可变数据类型上使用协方差,所以这一切都有效,因为 ListOption 是不可变的。像 ArrayBuffer 这样的可变集合的类型参数是不变的。 (注意:Java 的内置数组是协变的,这就是 unsound 并会导致各种问题)

,

这种脱糖机制究竟是如何工作的?特别是 为什么 Some 默认扩展 Option[T] 而 None 扩展 选项[无]?

Some 默认扩展 Option[T] 因为它有一个类型参数作为参数 (x: T) 并且被类似地(但不完全相同)作为一个案例类,而 {{1 }} 被视为“常规”枚举值,因为它没有参数列表定义。

我们可以通过添加None来进一步分析打字机编译阶段:

-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 } 扩展为 MySome,而 case class 扩展为 None 上定义的值。

Silvio provided the "specification" 用于 MyOption 定义(截至今天 (08/06/2021) 我找不到正式的定义),其中规则 4 和规则 7 是我们正在寻找的:

如果 E 是一个带有类型参数 Ts 的枚举类,那么它的伴生对象中有一个 case,没有 extends 子句

enum

...

对于C没有类型参数的情况,假设E的类型参数是

case C <params> <body>

其中每个方差 Vi 是“+”或“-”。然后案例扩展到

V1 T1 > L1 <: U1,Vn Tn >: Ln <: Un      (n > 0)

对于 case C <params> extends E[B1,Bn] <body> 情况:

不带类型参数的枚举类 E 的 case C 展开

None

这里,$new 是一个私有方法,它创建一个 E 的实例(见下文)。


这似乎是一种定义 Option 的奇怪方式。

不是真的,仔细想想还是有道理的。在这种情况下,val C = $new(n,"C") 被视为单例,并且由于 None 的参数类型是协变的,而 Nothing is a bottom type which means it inherits all other types in Scala,它适用于任何赋值。

,
  1. 脱糖是编译器阶段的一部分,您需要了解的更重要的是 scala3 中的 enum 替换了 scala 2 的 coproduct/sum 类型。 它本质上是一个标记联合类型。

  2. Nothing 是底部类型,因此它扩展了所有内容。它是 Any(root object) 的对偶

ps:如果你愿意,我可以扩展那些,让我知道