Scala 3 — 书籍

代数数据类型

语言
此文档页面特定于 Scala 3,并且可能涵盖 Scala 2 中不可用的新概念。除非另有说明,此页面中的所有代码示例均假定你使用的是 Scala 3。

代数数据类型 (ADT) 可以使用 enum 构造创建,因此在查看 ADT 之前,我们将简要回顾枚举。

枚举

枚举用于定义由一组命名值组成的类型

enum Color:
  case Red, Green, Blue

可以看作是

enum Color:
  case Red   extends Color
  case Green extends Color
  case Blue  extends Color

参数

枚举可以参数化

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)

这样,每个不同的变体都有一个值成员 rgb,该值成员被分配相应的值

println(Color.Green.rgb) // prints 65280

自定义定义

枚举也可以有自定义定义

enum Planet(mass: Double, radius: Double):

  private final val G = 6.67300E-11
  def surfaceGravity = G * mass / (radius * radius)
  def surfaceWeight(otherMass: Double) =  otherMass * surfaceGravity

  case Mercury extends Planet(3.303e+23, 2.4397e6)
  case Venus   extends Planet(4.869e+24, 6.0518e6)
  case Earth   extends Planet(5.976e+24, 6.37814e6)
  // 5 or 6 more planets ...

像类和 case 类一样,你还可以为枚举定义一个伴生对象

object Planet:
  def main(args: Array[String]) =
    val earthWeight = args(0).toDouble
    val mass = earthWeight / Earth.surfaceGravity
    for (p <- values)
      println(s"Your weight on $p is ${p.surfaceWeight(mass)}")

代数数据类型 (ADT)

enum 概念足够通用,还可以支持代数数据类型 (ADT) 及其广义版本 (GADT)。以下示例展示了如何将 Option 类型表示为 ADT

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

此示例创建一个 Option 枚举,其中协变类型参数 T 包含两个情况,SomeNoneSome 使用值参数 x 进行参数化;这是编写扩展 Optioncase 类的简写。由于 None 未进行参数化,因此将其视为普通 enum 值。

上一个示例中省略的 extends 子句也可以显式给出

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

与普通 enum 值一样,enum 的情况在 enum 伴随对象中定义,因此它们被称为 Option.SomeOption.None(除非使用导入“提取”定义)

scala> Option.Some("hello")
val res1: t2.Option[String] = Some(hello)

scala> Option.None
val res2: t2.Option[Nothing] = None

与其他枚举用法一样,ADT 可以定义其他方法。例如,这里再次展示 Option,其中在伴随对象中包含一个 isDefined 方法和一个 Option(...) 构造函数

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

  def isDefined: Boolean = this match
    case None => false
    case Some(_) => true

object Option:
  def apply[T >: Null](x: T): Option[T] =
    if (x == null) None else Some(x)

枚举和 ADT 共享相同的语法结构,因此可以将它们简单地视为光谱的两端,并且完全可以构建混合体。例如,以下代码给出了 Color 的实现,其中包含三个枚举值或采用 RGB 值的参数化情况

enum Color(val rgb: Int):
  case Red   extends Color(0xFF0000)
  case Green extends Color(0x00FF00)
  case Blue  extends Color(0x0000FF)
  case Mix(mix: Int) extends Color(mix)

递归枚举

到目前为止,我们定义的所有枚举都由不同变体的值或情况类组成。枚举也可以是递归的,如下面编码自然数的示例所示

enum Nat:
  case Zero
  case Succ(n: Nat)

例如,值 Succ(Succ(Zero)) 在一元编码中表示数字 2。可以以非常相似的方式定义列表

enum List[+A]:
  case Nil
  case Cons(head: A, tail: List[A])

广义代数数据类型 (GADT)

上述枚举表示法非常简洁,可作为对数据类型建模的完美起点。由于我们始终可以更明确,因此还可以表示功能更强大的类型:广义代数数据类型 (GADT)。

下面是一个 GADT 示例,其中类型参数 (T) 指定存储在框中的内容

enum Box[T](contents: T):
  case IntBox(n: Int) extends Box[Int](n)
  case BoolBox(b: Boolean) extends Box[Boolean](b)

对特定构造函数 (IntBoxBoolBox) 进行模式匹配可恢复类型信息

def extract[T](b: Box[T]): T = b match
  case IntBox(n)  => n + 1
  case BoolBox(b) => !b

仅在第一种情况下返回 Int 是安全的,因为我们从模式匹配中知道输入是 IntBox

枚举去糖

从概念上讲,枚举可以被认为是定义了一个密封类及其伴随对象。我们来看看上面 Color 枚举的去糖

sealed abstract class Color(val rgb: Int) extends scala.reflect.Enum
object Color:
  case object Red extends Color(0xFF0000) { def ordinal = 0 }
  case object Green extends Color(0x00FF00) { def ordinal = 1 }
  case object Blue extends Color(0x0000FF) { def ordinal = 2 }
  case class Mix(mix: Int) extends Color(mix) { def ordinal = 3 }

  def fromOrdinal(ordinal: Int): Color = ordinal match
    case 0 => Red
    case 1 => Green
    case 2 => Blue
    case _ => throw new NoSuchElementException(ordinal.toString)

请注意,上述去糖是简化的,我们故意省略了 一些细节

虽然可以使用其他构造手动对枚举进行编码,但使用枚举更简洁,还附带一些其他实用程序(例如 fromOrdinal 方法)。

此页面的贡献者