在 GitHub 上編輯此頁面

類型類別推導

類型類別推導是一種自動產生滿足一些簡單條件的類型類別的 given 實例的方法。在此意義上,類型類別是任何特質或類別,其中單一類型參數決定要操作的類型,以及特殊情況 CanEqual。常見範例包括 EqOrderingShow。例如,給定以下 Tree 代數資料類型 (ADT)

enum Tree[T] derives Eq, Ordering, Show:
  case Branch(left: Tree[T], right: Tree[T])
  case Leaf(elem: T)

derives 子句會在 Tree 的伴隨物件中產生 EqOrderingShow 類型類別的以下 given 實例

given [T: Eq]       : Eq[Tree[T]]       = Eq.derived
given [T: Ordering] : Ordering[Tree[T]] = Ordering.derived
given [T: Show]     : Show[Tree[T]]     = Show.derived

我們說 Tree推導類型,而 EqOrderingShow 實例是衍生實例

注意:derived 可以手動使用,這在您無法控制定義時很有用。例如,我們可以像這樣為 Option 實作 Ordering

given [T: Ordering]: Ordering[Option[T]] = Ordering.derived

如果您能改用 derives 子句,則不建議直接參照 derived 成員。

所有資料類型都可以有 derives 子句。本文主要關注也有一個可用的 Mirror 類型類別的 given 實例的資料類型。

精確機制

在以下情況,當類型參數被列舉,且第一個索引評估為大於最後一個索引的值時,實際上沒有參數,例如:A[T_2, ..., T_1] 表示 A

對於類別/特徵/物件/列舉 DerivingType[T_1, ..., T_N] derives TC,在 DerivingType 的伴侶物件中會建立一個衍生實例(如果它是物件,則為 DerivingType 本身)。

衍生實例的一般「形狀」如下

given [...](using ...): TC[ ... DerivingType[...] ... ] = TC.derived

TC.derived 應該是一個符合左側預期類型的表達式,可能使用術語和/或類型推論進行說明。

注意: TC.derived 是正常存取,因此如果有多個 TC.derived 定義,則會套用超載解析。

衍生實例的精確外觀取決於 DerivingTypeTC 的具體情況,我們首先檢查 TC

TC 採用 1 個參數 F

因此 TC 被定義為 TC[F[A_1, ..., A_K]](如果 K == 0,則為 TC[F]),其中 F 為某個參數。根據參數的種類,還有另外兩種情況

FDerivingType 的所有參數都有種類 *

注意: 在這種情況下,K == 0

然後,產生的實例為

given [T_1: TC, ..., T_N: TC]: TC[DerivingType[T_1, ..., T_N]] = TC.derived

這是最常見的情況,也是引言中強調的情況。

注意: [T_i: TC, ...] 會引入 (using TC[T_i], ...),有關更多資訊,請參閱 內容界限。這允許 derived 成員存取這些證據。

注意: 如果 N == 0,則上述表示

given TC[DerivingType] = TC.derived

例如,類別

case class Point(x: Int, y: Int) derives Ordering

會產生實例

object Point:
  ...
  given Ordering[Point] = Ordering.derived

FDerivingType 在右側有相符種類的參數

本節涵蓋您可以從右側開始配對 FDerivingType 的參數的情況,以便它們成對具有相同的種類,且 FDerivingType(或兩者)的所有參數都已用盡。F 也必須至少有一個參數。

一般形狀為

given [...]: TC[ [...] =>> DerivingType[...] ] = TC.derived

當然,TCDerivingType 套用於正確類型的類型。

為了讓此運作,我們將其拆分為 3 個案例

如果 FDerivingType 取用相同數量的引數 (N == K)

given TC[DerivingType] = TC.derived
// simplified form of:
given TC[ [A_1, ..., A_K] =>> DerivingType[A_1, ..., A_K] ] = TC.derived

如果 DerivingType 取用比 F 少的引數 (N < K),我們僅使用類型 lambda 中最右邊的參數

given TC[ [A_1, ..., A_K] =>> DerivingType[A_(K-N+1), ..., A_K] ] = TC.derived

// if DerivingType takes no arguments (N == 0), the above simplifies to:
given TC[ [A_1, ..., A_K] =>> DerivingType ] = TC.derived

如果 F 取用比 DerivingType 少的引數 (K < N),我們使用給定的類型參數填入剩下的最左邊的插槽

given [T_1, ... T_(N-K)]: TC[[A_1, ..., A_K] =>> DerivingType[T_1, ... T_(N-K), A_1, ..., A_K]] = TC.derived

TCCanEqual 類型類別

因此,我們有:DerivingType[T_1, ..., T_N] derives CanEqual

U_1, ..., U_M 成為 DerivingType* 類型的參數。(這些是 T_i 的子集)

然後,產生的實例為

given [T_1L, T_1R, ..., T_NL, T_NR]                            // every parameter of DerivingType twice
      (using CanEqual[U_1L, U_1R], ..., CanEqual[U_ML, U_MR]): // only parameters of DerivingType with kind *
        CanEqual[DerivingType[T_1L, ..., T_NL], DerivingType[T_1R, ..., T_NR]] = // again, every parameter
          CanEqual.derived

T_i 的界限正確處理,例如:T_2 <: T_1 變成 T_2L <: T_1L

例如,類別

class MyClass[A, G[_]](a: A, b: G[B]) derives CanEqual

產生以下給定的實例

object MyClass:
  ...
  given [A_L, A_R, G_L[_], G_R[_]](using CanEqual[A_L, A_R]): CanEqual[MyClass[A_L, G_L], MyClass[A_R, G_R]] = CanEqual.derived

TC 不適用於自動衍生

拋出錯誤。

確切的錯誤取決於上述哪個條件失敗。例如,如果 TC 取用超過 1 個參數且不是 CanEqual,則錯誤為 DerivingType 無法與 TC 的類型引數統一

所有資料類型都可以有 derives 子句。本文件其餘部分主要關注同時具有 Mirror 類型類別的給定實例的資料類型。

Mirror

scala.deriving.Mirror 類型類別實例提供有關類型組成和標記的類型層級資訊。它們還提供最低限度的術語層級基礎結構,以允許更高級別的函式庫提供全面的衍生支援。

Mirror 類型類別的實例由編譯器自動產生,無條件適用於

  • 列舉和列舉案例,
  • 案例物件。

Mirror 的實例也根據條件產生,適用於

  • 建構函式在呼叫位置可見的案例類別(如果伴隨物件不是案例物件,則永遠為真)
  • 密封類別和密封特徵,其中
    • 存在至少一個子案例,
    • 每個子案例都可從父定義中存取,
    • 如果密封特徵/類別沒有伴隨物件,則每個子案例都可透過要鏡像化的類型的前置詞從呼叫位置存取,
    • 且編譯器可以為每個子案例產生 Mirror 類型類別實例。

scala.deriving.Mirror 類型類別定義如下

sealed trait Mirror:

  /** the type being mirrored */
  type MirroredType

  /** the type of the elements of the mirrored type */
  type MirroredElemTypes

  /** The mirrored *-type */
  type MirroredMonoType

  /** The name of the type */
  type MirroredLabel <: String

  /** The names of the elements of the type */
  type MirroredElemLabels <: Tuple

object Mirror:

  /** The Mirror for a product type */
  trait Product extends Mirror:

    /** Create a new instance of type `T` with elements
     *  taken from product `p`.
     */
    def fromProduct(p: scala.Product): MirroredMonoType

  trait Sum extends Mirror:

    /** The ordinal number of the case class of `x`.
     *  For enums, `ordinal(x) == x.ordinal`
     */
    def ordinal(x: MirroredMonoType): Int

end Mirror

乘積類型(即案例類別和物件,以及列舉案例)具有鏡像,這些鏡像是 Mirror.Product 的子類型。和類型(即密封類別或具有乘積子類的特徵,以及列舉)具有鏡像是 Mirror.Sum 的子類型。

對於上述的 Tree ADT,編譯器將自動提供下列 Mirror 實例,

// Mirror for Tree
new Mirror.Sum:
  type MirroredType = Tree
  type MirroredElemTypes[T] = (Branch[T], Leaf[T])
  type MirroredMonoType = Tree[_]
  type MirroredLabel = "Tree"
  type MirroredElemLabels = ("Branch", "Leaf")

  def ordinal(x: MirroredMonoType): Int = x match
    case _: Branch[_] => 0
    case _: Leaf[_] => 1

// Mirror for Branch
new Mirror.Product:
  type MirroredType = Branch
  type MirroredElemTypes[T] = (Tree[T], Tree[T])
  type MirroredMonoType = Branch[_]
  type MirroredLabel = "Branch"
  type MirroredElemLabels = ("left", "right")

  def fromProduct(p: Product): MirroredMonoType =
    new Branch(...)

// Mirror for Leaf
new Mirror.Product:
  type MirroredType = Leaf
  type MirroredElemTypes[T] = Tuple1[T]
  type MirroredMonoType = Leaf[_]
  type MirroredLabel = "Leaf"
  type MirroredElemLabels = Tuple1["elem"]

  def fromProduct(p: Product): MirroredMonoType =
    new Leaf(...)

如果無法為特定類型自動產生 Mirror,將會出現錯誤,說明它既不是受支援的和類型也不是乘積類型的原因。例如,如果 A 是未密封的特徵,

No given instance of type deriving.Mirror.Of[A] was found for parameter x of method summon in object Predef. Failed to synthesize an instance of type deriving.Mirror.Of[A]:
     * trait A is not a generic product because it is not a case class
     * trait A is not a generic sum because it is not a sealed trait

請注意 Mirror 類型的下列屬性,

  • 屬性使用類型而非術語編碼。這表示除非使用,否則它們沒有執行時間佔用空間,而且它們是供 Scala 3 的元程式設計功能使用的編譯時間功能。
  • 對於鏡像類型是局部類別或內部類別沒有限制。
  • MirroredTypeMirroredElemTypes 的類型與鏡像是其執行個體的資料類型的類型相符。這允許 Mirror 支援各種 ADT。
  • 沒有針對總和或乘積的明確表示類型(即,沒有像 Scala 2 版本的 Shapeless 中的 HListCoproduct 類型)。相反,資料類型的子類型集合由一個普通(可能參數化)的元組類型表示。Scala 3 的元程式設計功能可用於原樣處理這些元組類型,並且可以在它們之上建立更高級別的函式庫。
  • 對於乘積和總和類型,MirroredElemTypes 的元素按定義順序排列(即,Branch[T]MirroredElemTypes 中位於 TreeLeaf[T] 之前,因為 Branch 在原始檔中定義在 Leaf 之前)。這表示 Mirror.Sum 在這方面與 Scala 2 中 Shapeless 的 ADT 通用表示不同,在後者中,建構函式按名稱字母順序排列。
  • 方法 ordinalfromProduct 是根據 MirroredMonoType 定義的,MirroredMonoType 是類型 * 的類型,可透過對 MirroredType 的類型參數使用萬用字元來取得。

使用 Mirror 實作 derived

如前所見,類型類別 TC[_]derived 方法的簽章和實作是任意的,但我們預期它通常會是下列形式

import scala.deriving.Mirror

inline def derived[T](using Mirror.Of[T]): TC[T] = ...

亦即,derived 方法採用型態 Mirror(某個子型態)的內容參數來定義衍生型態 T 的形狀,並根據該形狀計算型態類別實作。這便是提供具有 derives 子句的 ADT 的提供者,在型態類別實例的衍生中所需要知道的一切。

請注意,derived 方法可能會間接擁有內容 Mirror 參數(例如透過擁有內容參數的內容引數,而該內容引數又擁有內容 Mirror 參數),或完全沒有(例如它們可能會使用完全不同的使用者提供機制,例如使用 Scala 3 巨集或執行時期反射)。我們預期(直接或間接)基於 Mirror 的實作將是最常見的,而這是這份文件所強調的。

型態類別作者很可能會使用更高級的衍生或泛型程式設計函式庫來實作 derived 方法。以下提供一個範例,說明如何僅使用上述低階工具和 Scala 3 的一般巨程式設計功能來實作 derived 方法。我們並不預期型態類別作者通常會以這種方式實作 derived 方法,不過這個演練可以作為我們預期一般型態類別作者將會使用的更高級衍生函式庫作者的指南(如需此類函式庫的完整範例,請參閱 Shapeless 3)。

如何使用低階機制撰寫型態類別 derived 方法

我們將在這個範例中用來實作型態類別 derived 方法的低階技術,利用 Scala 3 中的三個新的型態層級建構:內聯方法、內聯比對,以及透過 summonInlinesummonFrom 的內隱搜尋。在 Eq 型態類別的這個定義中,

trait Eq[T]:
  def eqv(x: T, y: T): Boolean

我們需要在 Eq 的伴生物件上實作一個方法 Eq.derived,它會針對給定的 Mirror[T] 產生一個 Eq[T] 的實例。以下是一個可能的實作:

import scala.deriving.Mirror

inline def derived[T](using m: Mirror.Of[T]): Eq[T] =
  lazy val elemInstances = summonInstances[T, m.MirroredElemTypes] // (1)
  inline m match                                                   // (2)
    case s: Mirror.SumOf[T]     => eqSum(s, elemInstances)
    case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)

請注意,derived 被定義為 inline def。這表示這個方法會在所有呼叫位置內聯(例如編譯器產生的實例定義,在具有 deriving Eq 子句的 ADT 伴生物件中)。

過度使用複雜程式碼的內聯可能會造成成本(表示編譯時間較慢),因此我們應該小心限制呼叫 derived 的次數。例如,在計算總和類型的實例時,可能需要遞迴呼叫 derived 來計算其每個子案例的實例。該子案例反過來可能是一個乘積類型,它宣告一個回指父總和類型的欄位。為了計算此欄位的實例,我們不應該遞迴呼叫 derived,而應該從內容中呼叫。通常,找到的給定實例將是最初呼叫 derived 的根給定實例。

derived 的主體(1)首先實體化要為其衍生實例的類型的所有子類型的 Eq 實例。這可能是總和類型的所有分支或乘積類型的所有欄位。summonInstances 的實作是 inline,並使用 Scala 3 的 summonInline 建構來收集實例作為 List

inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] =
  inline erasedValue[Elems] match
    case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems]
    case _: EmptyTuple => Nil

inline def deriveOrSummon[T, Elem]: Eq[Elem] =
  inline erasedValue[Elem] match
    case _: T => deriveRec[T, Elem]
    case _    => summonInline[Eq[Elem]]

inline def deriveRec[T, Elem]: Eq[Elem] =
  inline erasedValue[T] match
    case _: Elem => error("infinite recursive derivation")
    case _       => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation

有了子類型的實例,derived 方法使用 inline match 來分派到可以為總和或乘積建構實例的方法(2)。請注意,因為 derivedinline,所以比對會在編譯時解析,並且只有比對案例的右側會內聯到產生的程式碼中,類型會根據比對結果進行精緻化。

在總和情況下,eqSum,我們使用 eqv 參數的執行時間 ordinal 值,首先檢查兩個值是否為 ADT 的相同子類型 (3),然後,如果它們是,則進一步根據適當的 ADT 子類型的 Eq 執行個體使用輔助方法 check (4) 進行相等性測試。

import scala.deriving.Mirror

def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] =
  new Eq[T]:
    def eqv(x: T, y: T): Boolean =
      val ordx = s.ordinal(x)                            // (3)
      (s.ordinal(y) == ordx) && check(x, y, elems(ordx)) // (4)

在乘積情況下,eqProduct,我們根據資料類型的欄位的 Eq 執行個體,針對 eqv 參數的執行時間值進行相等性測試,作為乘積 (5),

import scala.deriving.Mirror

def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] =
  new Eq[T]:
    def eqv(x: T, y: T): Boolean =
      iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check)

eqSumeqProduct 都有按名稱參數 elems,因為傳遞的參數是對 lazy elemInstances 值的參考。

將所有這些組合在一起,我們有以下完整的實作,

import scala.collection.AbstractIterable
import scala.compiletime.{erasedValue, error, summonInline}
import scala.deriving.*

inline def summonInstances[T, Elems <: Tuple]: List[Eq[?]] =
  inline erasedValue[Elems] match
    case _: (elem *: elems) => deriveOrSummon[T, elem] :: summonInstances[T, elems]
    case _: EmptyTuple => Nil

inline def deriveOrSummon[T, Elem]: Eq[Elem] =
  inline erasedValue[Elem] match
    case _: T => deriveRec[T, Elem]
    case _    => summonInline[Eq[Elem]]

inline def deriveRec[T, Elem]: Eq[Elem] =
  inline erasedValue[T] match
    case _: Elem => error("infinite recursive derivation")
    case _       => Eq.derived[Elem](using summonInline[Mirror.Of[Elem]]) // recursive derivation

trait Eq[T]:
  def eqv(x: T, y: T): Boolean

object Eq:
  given Eq[Int] with
    def eqv(x: Int, y: Int) = x == y

  def check(x: Any, y: Any, elem: Eq[?]): Boolean =
    elem.asInstanceOf[Eq[Any]].eqv(x, y)

  def iterable[T](p: T): Iterable[Any] = new AbstractIterable[Any]:
    def iterator: Iterator[Any] = p.asInstanceOf[Product].productIterator

  def eqSum[T](s: Mirror.SumOf[T], elems: => List[Eq[?]]): Eq[T] =
    new Eq[T]:
      def eqv(x: T, y: T): Boolean =
        val ordx = s.ordinal(x)
        (s.ordinal(y) == ordx) && check(x, y, elems(ordx))

  def eqProduct[T](p: Mirror.ProductOf[T], elems: => List[Eq[?]]): Eq[T] =
    new Eq[T]:
      def eqv(x: T, y: T): Boolean =
        iterable(x).lazyZip(iterable(y)).lazyZip(elems).forall(check)

  inline def derived[T](using m: Mirror.Of[T]): Eq[T] =
    lazy val elemInstances = summonInstances[T, m.MirroredElemTypes]
    inline m match
      case s: Mirror.SumOf[T]     => eqSum(s, elemInstances)
      case p: Mirror.ProductOf[T] => eqProduct(p, elemInstances)
end Eq

我們可以像這樣針對一個簡單的 ADT 進行測試,

enum Lst[+T] derives Eq:
  case Cns(t: T, ts: Lst[T])
  case Nl

extension [T](t: T) def ::(ts: Lst[T]): Lst[T] = Lst.Cns(t, ts)

@main def test(): Unit =
  import Lst.*
  val eqoi = summon[Eq[Lst[Int]]]
  assert(eqoi.eqv(23 :: 47 :: Nl, 23 :: 47 :: Nl))
  assert(!eqoi.eqv(23 :: Nl, 7 :: Nl))
  assert(!eqoi.eqv(23 :: Nl, Nl))

在這種情況下,經過一點潤色後,為 Lst 衍生的 Eq 執行個體產生的程式碼如下所示,

given derived$Eq[T](using eqT: Eq[T]): Eq[Lst[T]] =
  eqSum(summon[Mirror.Of[Lst[T]]], {/* cached lazily */
    List(
      eqProduct(summon[Mirror.Of[Cns[T]]], {/* cached lazily */
        List(summon[Eq[T]], summon[Eq[Lst[T]]])
      }),
      eqProduct(summon[Mirror.Of[Nl.type]], {/* cached lazily */
        Nil
      })
    )
  })

elemInstances 上的 lazy 修改項對於防止在遞迴類型(例如 Lst)的衍生執行個體中發生無限遞迴是必要的。

可以採用不同的方法來定義 derived 方法。例如,更積極地內聯變體使用 Scala 3 巨集,儘管對於類型類別作者來說,撰寫比上面的範例更複雜,但可以產生類型類別的程式碼,例如 Eq,它消除了所有抽象人工製品(例如上述子執行個體的 Lists),並產生與程式設計師手寫的程式碼無法區分的程式碼。作為第三個範例,使用較高層級的函式庫(例如 Shapeless),類型類別作者可以定義等效的 derived 方法為,

given eqSum[A](using inst: => K0.CoproductInstances[Eq, A]): Eq[A] with
  def eqv(x: A, y: A): Boolean = inst.fold2(x, y)(false)(
    [t] => (eqt: Eq[t], t0: t, t1: t) => eqt.eqv(t0, t1)
  )

given eqProduct[A](using inst: => K0.ProductInstances[Eq, A]): Eq[A] with
  def eqv(x: A, y: A): Boolean = inst.foldLeft2(x, y)(true: Boolean)(
    [t] => (acc: Boolean, eqt: Eq[t], t0: t, t1: t) =>
      Complete(!eqt.eqv(t0, t1))(false)(true)
  )

inline def derived[A](using gen: K0.Generic[A]): Eq[A] =
  gen.derive(eqProduct, eqSum)

這裡描述的架構啟用了這三種方法,而沒有強制要求任何一種方法。

有關如何使用巨集撰寫類型類別 derived 方法的簡要說明,請閱讀 如何使用巨集撰寫類型類別 derived 方法

語法

Template          ::=  InheritClauses [TemplateBody]
EnumDef           ::=  id ClassConstr InheritClauses EnumBody
InheritClauses    ::=  [‘extends’ ConstrApps] [‘derives’ QualId {‘,’ QualId}]
ConstrApps        ::=  ConstrApp {‘with’ ConstrApp}
                    |  ConstrApp {‘,’ ConstrApp}

注意:為了對齊 extends 子句和 derives 子句,Scala 3 也允許多個延伸類型以逗號分隔。因此,下列內容現在合法

class A extends B, C { ... }

它等同於舊形式

class A extends B with C { ... }

說明

此類型類別推導架構故意非常小且低階。編譯器產生的 Mirror 實例中基本上有兩個基礎架構,

  • 編碼鏡像類型屬性的類型成員。
  • 一個用於泛型處理鏡像類型項目的最小值層級機制。

Mirror 基礎架構可以視為現有案例類別 Product 基礎架構的延伸:一般來說,Mirror 類型將由 ADT 伴隨物件實作,因此類型成員和 ordinalfromProduct 方法將是該物件的成員。此設計決策的主要動機,以及透過類型而非項目編碼屬性的決策,是為了讓功能的位元組碼和執行時間佔用空間保持足夠小,以提供 無條件Mirror 實例。

雖然 Mirror 精確地透過類型成員編碼屬性,但值層級的 ordinalfromProduct 有點弱型別(因為它們是根據 MirroredMonoType 定義的),就像 Product 的成員一樣。這表示泛型類型類別的程式碼必須確保類型探索和值選取同步進行,並且必須在某些地方使用強制轉換來斷言此一致性。如果泛型類型類別撰寫正確,這些強制轉換永遠不會失敗。

不過,正如所提到的,編譯器提供的機制故意非常低階,預期高階型類別衍生和泛型程式設計函式庫將建立在這個和 Scala 3 的其他元程式設計設施之上,以對型類別作者和一般使用者隱藏這些低階細節。Shapeless 和 Magnolia 風格的型類別衍生是可能的(Shapeless 3 的原型結合了 Shapeless 2 和 Magnolia 的面向,已與此語言功能一起開發)如同由 Scala 3 的新引號/拼接巨集和內嵌設施支援的更激進的內嵌風格。