類型類別推導
類型類別推導是一種自動產生滿足一些簡單條件的類型類別的 given 實例的方法。在此意義上,類型類別是任何特質或類別,其中單一類型參數決定要操作的類型,以及特殊情況 CanEqual
。常見範例包括 Eq
、Ordering
或 Show
。例如,給定以下 Tree
代數資料類型 (ADT)
enum Tree[T] derives Eq, Ordering, Show:
case Branch(left: Tree[T], right: Tree[T])
case Leaf(elem: T)
derives
子句會在 Tree
的伴隨物件中產生 Eq
、Ordering
和 Show
類型類別的以下 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
是推導類型,而 Eq
、Ordering
和 Show
實例是衍生實例。
注意: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
定義,則會套用超載解析。
衍生實例的精確外觀取決於 DerivingType
和 TC
的具體情況,我們首先檢查 TC
TC
採用 1 個參數 F
因此 TC
被定義為 TC[F[A_1, ..., A_K]]
(如果 K == 0
,則為 TC[F]
),其中 F
為某個參數。根據參數的種類,還有另外兩種情況
F
和 DerivingType
的所有參數都有種類 *
注意: 在這種情況下,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
F
和 DerivingType
在右側有相符種類的參數
本節涵蓋您可以從右側開始配對 F
和 DerivingType
的參數的情況,以便它們成對具有相同的種類,且 F
或 DerivingType
(或兩者)的所有參數都已用盡。F
也必須至少有一個參數。
一般形狀為
given [...]: TC[ [...] =>> DerivingType[...] ] = TC.derived
當然,TC
和 DerivingType
套用於正確類型的類型。
為了讓此運作,我們將其拆分為 3 個案例
如果 F
和 DerivingType
取用相同數量的引數 (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
TC
是 CanEqual
類型類別
因此,我們有: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 的元程式設計功能使用的編譯時間功能。
- 對於鏡像類型是局部類別或內部類別沒有限制。
MirroredType
和MirroredElemTypes
的類型與鏡像是其執行個體的資料類型的類型相符。這允許Mirror
支援各種 ADT。- 沒有針對總和或乘積的明確表示類型(即,沒有像 Scala 2 版本的 Shapeless 中的
HList
或Coproduct
類型)。相反,資料類型的子類型集合由一個普通(可能參數化)的元組類型表示。Scala 3 的元程式設計功能可用於原樣處理這些元組類型,並且可以在它們之上建立更高級別的函式庫。 - 對於乘積和總和類型,
MirroredElemTypes
的元素按定義順序排列(即,Branch[T]
在MirroredElemTypes
中位於Tree
的Leaf[T]
之前,因為Branch
在原始檔中定義在Leaf
之前)。這表示Mirror.Sum
在這方面與 Scala 2 中 Shapeless 的 ADT 通用表示不同,在後者中,建構函式按名稱字母順序排列。 - 方法
ordinal
和fromProduct
是根據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 中的三個新的型態層級建構:內聯方法、內聯比對,以及透過 summonInline
或 summonFrom
的內隱搜尋。在 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)。請注意,因為 derived
是 inline
,所以比對會在編譯時解析,並且只有比對案例的右側會內聯到產生的程式碼中,類型會根據比對結果進行精緻化。
在總和情況下,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)
eqSum
和 eqProduct
都有按名稱參數 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 伴隨物件實作,因此類型成員和 ordinal
或 fromProduct
方法將是該物件的成員。此設計決策的主要動機,以及透過類型而非項目編碼屬性的決策,是為了讓功能的位元組碼和執行時間佔用空間保持足夠小,以提供 無條件 的 Mirror
實例。
雖然 Mirror
精確地透過類型成員編碼屬性,但值層級的 ordinal
和 fromProduct
有點弱型別(因為它們是根據 MirroredMonoType
定義的),就像 Product
的成員一樣。這表示泛型類型類別的程式碼必須確保類型探索和值選取同步進行,並且必須在某些地方使用強制轉換來斷言此一致性。如果泛型類型類別撰寫正確,這些強制轉換永遠不會失敗。
不過,正如所提到的,編譯器提供的機制故意非常低階,預期高階型類別衍生和泛型程式設計函式庫將建立在這個和 Scala 3 的其他元程式設計設施之上,以對型類別作者和一般使用者隱藏這些低階細節。Shapeless 和 Magnolia 風格的型類別衍生是可能的(Shapeless 3 的原型結合了 Shapeless 2 和 Magnolia 的面向,已與此語言功能一起開發)如同由 Scala 3 的新引號/拼接巨集和內嵌設施支援的更激進的內嵌風格。