Scala 3 — 書籍

變異

語言

類型參數變異控制參數化類型的子類型化(例如類別或特質)。

為了說明變異,讓我們假設以下類型定義

trait Item { def productNumber: String }
trait Buyable extends Item { def price: Int }
trait Book extends Buyable { def isbn: String }

讓我們也假設以下參數化類型

// an example of an invariant type
trait Pipeline[T] {
  def process(t: T): T
}

// an example of a covariant type
trait Producer[+T] {
  def make: T
}

// an example of a contravariant type
trait Consumer[-T] {
  def take(t: T): Unit
}
// an example of an invariant type
trait Pipeline[T]:
  def process(t: T): T

// an example of a covariant type
trait Producer[+T]:
  def make: T

// an example of a contravariant type
trait Consumer[-T]:
  def take(t: T): Unit

一般而言,有下列三種變異模式

  • 不變—預設值,寫成 Pipeline[T]
  • 協變—加上 + 註解,例如 Producer[+T]
  • 逆變—加上 - 註解,例如 Consumer[-T]

我們現在將詳細說明此註解的意義以及我們使用它的原因。

不變類型

預設情況下,像 Pipeline 之類的類型在其類型參數(本例中為 T)中是不變的。這表示像 Pipeline[Item]Pipeline[Buyable]Pipeline[Book] 之類的類型彼此之間沒有子類型關係

這是理所當然的!假設下列方法使用兩個 Pipeline[Buyable] 類型的值,並根據價格將其參數 b 傳遞給其中一個

def oneOf(
  p1: Pipeline[Buyable],
  p2: Pipeline[Buyable],
  b: Buyable
): Buyable = {
  val b1 = p1.process(b)
  val b2 = p2.process(b)
  if (b1.price < b2.price) b1 else b2
 } 
def oneOf(
  p1: Pipeline[Buyable],
  p2: Pipeline[Buyable],
  b: Buyable
): Buyable =
  val b1 = p1.process(b)
  val b2 = p2.process(b)
  if b1.price < b2.price then b1 else b2

現在,回想一下我們的類型之間有下列子類型關係

Book <: Buyable <: Item

我們無法將 Pipeline[Book] 傳遞給方法 oneOf,因為在其實作中,我們使用 Buyable 類型的值呼叫 p1p2Pipeline[Book] 預期 Book,這可能會導致執行時期錯誤。

我們無法傳遞 Pipeline[Item],因為對其呼叫 process 只保證會傳回 Item;然而,我們應該傳回 Buyable

為什麼是不變的?

事實上,類型 Pipeline 需要是不變的,因為它將其類型參數 T 同時用作參數傳回類型。基於相同原因,Scala 函式庫中的一些類型(例如 ArraySet)也是不變的

協變類型

與不變的 Pipeline 相反,類型 Producer 標記為協變,方法是在類型參數前加上 +。這是有效的,因為類型參數僅用於回傳位置

將其標記為協變表示我們可以在預期 Producer[Buyable] 的地方傳遞 (或回傳) Producer[Book]。事實上,這是合理的。Producer[Buyable].make 的類型僅承諾回傳 Buyable。作為 make 的呼叫者,我們也很樂意接受 Book,它是 Buyable 的子類型,也就是說,它至少Buyable

以下範例說明了這一點,其中函數 makeTwo 預期 Producer[Buyable]

def makeTwo(p: Producer[Buyable]): Int =
  p.make.price + p.make.price

傳遞書籍的生產者完全沒問題

val bookProducer: Producer[Book] = ???
makeTwo(bookProducer)

makeTwo 中呼叫 price 對書籍來說仍然有效。

不可變容器的協變類型

在處理不可變容器時,您會遇到許多協變類型,例如可以在標準函式庫中找到的類型 (例如 ListSeqVector 等)。

例如,ListVector 大約定義為

class List[+A] ...
class Vector[+A] ...

這樣,您可以在預期 List[Buyable] 的地方使用 List[Book]。這在直覺上也說得通:如果您預期的是可以購買的物品集合,那麼給您一個書籍集合應該是沒問題的。在我們的範例中,它們有一個額外的 ISBN 方法,但您可以忽略這些額外功能。

反變類型

與標記為協變的類型 Producer 相反,類型 Consumer 會在類型參數前加上 -,標記為反變。這是有效的,因為類型參數僅用於引數位置

將其標記為反變表示我們可以在預期 Consumer[Buyable] 的地方傳遞 (或傳回) Consumer[Item]。也就是說,我們有子類型關係 Consumer[Item] <: Consumer[Buyable]。請記住,對於類型 Producer,情況正好相反,我們有 Producer[Buyable] <: Producer[Item]

事實上,這是合理的。方法 Consumer[Item].take 接受 Item。作為 take 的呼叫方,我們也可以提供 Buyable,而 Consumer[Item] 會很樂意接受,因為 BuyableItem 的子類型,也就是說,它至少是一個 Item

消費者的反變類型

反變類型比協變類型少見得多。就像在我們的範例中,你可以將它們視為「消費者」。你可能會遇到的標記為反變的最重要的類型是函數

trait Function[-A, +B] {
  def apply(a: A): B
}
trait Function[-A, +B]:
  def apply(a: A): B

它的參數類型 A 標記為反變 A—它會消耗類型 A 的值。相反地,它的結果類型 B 標記為協變—它會產生類型 B 的值。

以下是說明函數上的變異註解所引發的子類型關係的一些範例

val f: Function[Buyable, Buyable] = b => b

// OK to return a Buyable where a Item is expected
val g: Function[Buyable, Item] = f

// OK to provide a Book where a Buyable is expected
val h: Function[Book, Buyable] = f

摘要

在本節中,我們遇到了三種不同的變異

  • 生產者通常是協變的,並使用 + 標記其類型參數。這也適用於不可變集合。
  • 消費者通常是反變的,並使用 - 標記其類型參數。
  • 既是生產者又是消費者的類型必須是不變的,並且不需要在其類型參數上做任何標記。像 Array 這樣的可變集合屬於此類別。

此頁面的貢獻者