在 GitHub 上編輯此頁面

實作型別類別

類型類別是一種抽象的參數化類型,可讓您在不使用子類型化的情況下,為任何封閉資料類型新增新行為。這在多種使用案例中都很有用,例如

  • 表達您不擁有的類型(來自標準或第三方程式庫)如何符合此類行為
  • 為多種類型表達此類行為,而不會涉及這些類型之間的子類型化關係(一個 extends 另一個)(例如:臨時多型

因此在 Scala 3 中,類型類別只是具有 1 個或多個參數的特質,其實作並非透過 extends 關鍵字定義,而是透過已提供實例。以下是一些常見類型類別的範例

半群和單元

以下是 Monoid 類型類別定義

trait SemiGroup[T]:
  extension (x: T) def combine (y: T): T

trait Monoid[T] extends SemiGroup[T]:
  def unit: T

以下是類型 StringMonoid 類型類別實作

given Monoid[String] with
  extension (x: String) def combine (y: String): String = x.concat(y)
  def unit: String = ""

對於類型 Int 可以寫成以下

given Monoid[Int] with
  extension (x: Int) def combine (y: Int): Int = x + y
  def unit: Int = 0

此單元現在可用作以下 combineAll 方法中的內容繫結

def combineAll[T: Monoid](xs: List[T]): T =
  xs.foldLeft(summon[Monoid[T]].unit)(_.combine(_))

若要移除 summon[...],我們可以定義 Monoid 物件,如下所示

object Monoid:
  def apply[T](using m: Monoid[T]) = m

這將允許重新撰寫 combineAll 方法,如下所示

def combineAll[T: Monoid](xs: List[T]): T =
  xs.foldLeft(Monoid[T].unit)(_.combine(_))

函數子

類型中的 Functor 提供其值「映射」的能力,亦即套用函數,在值內部進行轉換,同時記住其形狀。例如,修改集合中每個元素,而不會刪除或新增元素。我們可以使用 F 表示所有可以「映射」的類型。這是一個類型建構函數:其值的類型在提供類型引數時具體化。因此,我們寫成 F[_],暗示類型 F 將另一類型作為引數。泛型 Functor 的定義將寫成

trait Functor[F[_]]:
  def map[A, B](x: F[A], f: A => B): F[B]

可讀成:「類型建構函數 F[_]Functor 表示透過套用類型 A => B 的函數 f,將 F[A] 轉換成 F[B] 的能力」。我們在此將 Functor 定義稱為類型類別。這樣,我們可以為 List 類型定義 Functor 的執行個體

given Functor[List] with
  def map[A, B](x: List[A], f: A => B): List[B] =
    x.map(f) // List already has a `map` method

在範圍內使用這個 given 執行個體時,在任何需要具有 Functor 內容繫結的類型的任何地方,編譯器都會接受使用 List

例如,我們可以撰寫此類測試方法

def assertTransformation[F[_]: Functor, A, B](expected: F[B], original: F[A], mapping: A => B): Unit =
  assert(expected == summon[Functor[F]].map(original, mapping))

並以這種方式使用它,例如

assertTransformation(List("a1", "b1"), List("a", "b"), elt => s"${elt}1")

這是一個第一步,但在實務中,我們可能希望將 map 函數設定為可直接在類型 F 上存取的方法。這樣我們就可以直接對 F 的實例呼叫 map,並擺脫 summon[Functor[F]] 部分。如同單元範例中的 Monoid,extension 方法 有助於達成此目的。讓我們使用 extension 方法重新定義 Functor 類型類別。

trait Functor[F[_]]:
  extension [A](x: F[A])
    def map[B](f: A => B): F[B]

ListFunctor 實例現在變成

given Functor[List] with
  extension [A](xs: List[A])
    def map[B](f: A => B): List[B] =
      xs.map(f) // List already has a `map` method

它簡化了 assertTransformation 方法

def assertTransformation[F[_]: Functor, A, B](expected: F[B], original: F[A], mapping: A => B): Unit =
  assert(expected == original.map(mapping))

map 方法現在直接用於 original。它可用作 extension 方法,因為 original 的類型是 F[A],且定義 mapFunctor[F[A]] 給定實例在範圍內。

Monad

Functor[List] 中的 map 套用至類型為 A => B 的對應函數,會產生 List[B]。因此,將其套用至類型為 A => List[B] 的對應函數,會產生 List[List[B]]。為了避免管理清單清單,我們可能想要將值「扁平化」成單一清單。

這就是 Monad 的用武之地。類型 F[_]MonadFunctor[F],另加兩個運算

  • flatMap,在給定類型為 A => F[B] 的函數時,會將 F[A] 轉換成 F[B]
  • pure,會從單一值 A 建立 F[A]

以下是此定義在 Scala 3 中的翻譯

trait Monad[F[_]] extends Functor[F]:

  /** The unit value for a monad */
  def pure[A](x: A): F[A]

  extension [A](x: F[A])
    /** The fundamental composition operation */
    def flatMap[B](f: A => F[B]): F[B]

    /** The `map` operation can now be defined in terms of `flatMap` */
    def map[B](f: A => B) = x.flatMap(f.andThen(pure))

end Monad

List

可透過此 given 實例將 List 轉換成 monad

given listMonad: Monad[List] with
  def pure[A](x: A): List[A] =
    List(x)
  extension [A](xs: List[A])
    def flatMap[B](f: A => List[B]): List[B] =
      xs.flatMap(f) // rely on the existing `flatMap` method of `List`

由於 MonadFunctor 的子類型,因此 List 也是 functor。Functor 的 map 運算已由 Monad 特質提供,因此實例不需要明確定義它。

Option

Option 是具有相同行為的另一種類型

given optionMonad: Monad[Option] with
  def pure[A](x: A): Option[A] =
    Option(x)
  extension [A](xo: Option[A])
    def flatMap[B](f: A => Option[B]): Option[B] = xo match
      case Some(x) => f(x)
      case None => None

Reader

另一個 Monad 的範例是 Reader Monad,它作用於函式而非資料類型,例如 ListOption。它可用於結合多個需要相同參數的函式。例如,需要存取某些組態、內容、環境變數等的多個函式。

讓我們定義一個 Config 類型和兩個使用它的函式

trait Config
// ...
def compute(i: Int)(config: Config): String = ???
def show(str: String)(config: Config): Unit = ???

我們可能想要將 computeshow 結合成一個單一函式,接受 Config 作為參數,並顯示運算結果,我們希望使用 monad 來避免多次明確傳遞參數。因此,假設正確的 flatMap 運算,我們可以撰寫

def computeAndShow(i: Int): Config => Unit = compute(i).flatMap(show)

而非

show(compute(i)(config))(config)

讓我們定義這個 flatMap。首先,我們將定義一個名為 ConfigDependent 的類型,表示一個函式,當傳遞 Config 時會產生 Result

type ConfigDependent[Result] = Config => Result

monad 實例將如下所示

given configDependentMonad: Monad[ConfigDependent] with

  def pure[A](x: A): ConfigDependent[A] =
    config => x

  extension [A](x: ConfigDependent[A])
    def flatMap[B](f: A => ConfigDependent[B]): ConfigDependent[B] =
      config => f(x(config))(config)

end configDependentMonad

類型 ConfigDependent 可使用 類型 lambda 撰寫

type ConfigDependent = [Result] =>> Config => Result

使用此語法會將先前的 configDependentMonad 轉換為

given configDependentMonad: Monad[[Result] =>> Config => Result] with

  def pure[A](x: A): Config => A =
    config => x

  extension [A](x: Config => A)
    def flatMap[B](f: A => Config => B): Config => B =
      config => f(x(config))(config)

end configDependentMonad

我們很可能會希望將此模式與我們的 Config 特質以外的其他類型環境一起使用。Reader monad 允許我們將 Config 抽象為類型 參數,在以下定義中命名為 Ctx

given readerMonad[Ctx]: Monad[[X] =>> Ctx => X] with

  def pure[A](x: A): Ctx => A =
    ctx => x

  extension [A](x: Ctx => A)
    def flatMap[B](f: A => Ctx => B): Ctx => B =
      ctx => f(x(ctx))(ctx)

end readerMonad

摘要

類型類別 的定義以具有抽象成員的參數化類型表示,例如 trait。子類型多態性與使用 類型類別 的臨時多態性之間的主要差異在於 類型類別 的定義如何實作,相對於它作用的類型。對於 類型類別,其對具體類型的實作是透過 given 執行個體定義表示,並作為隱式引數與其作用的值一起提供。使用子類型多態性時,實作會混合到類別的父類別中,而且只需要一個單一術語就能執行多態性操作。類型類別解決方案的設定需要更多工作,但可擴充性較高:新增一個介面到類別需要變更該類別的原始碼。但相對地,類型類別的執行個體可以在任何地方定義。

總之,我們已經看到特質和既定執行個體,結合其他建構(例如擴充方法、內容界限和類型 lambda)允許簡潔且自然地表達類型類別