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 方法)。

此頁面的貢獻者