實作型別類別
類型類別是一種抽象的參數化類型,可讓您在不使用子類型化的情況下,為任何封閉資料類型新增新行為。這在多種使用案例中都很有用,例如
- 表達您不擁有的類型(來自標準或第三方程式庫)如何符合此類行為
- 為多種類型表達此類行為,而不會涉及這些類型之間的子類型化關係(一個
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
以下是類型 String
的 Monoid
類型類別實作
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]
List
的 Functor
實例現在變成
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]
,且定義 map
的 Functor[F[A]]
給定實例在範圍內。
Monad
將 Functor[List]
中的 map
套用至類型為 A => B
的對應函數,會產生 List[B]
。因此,將其套用至類型為 A => List[B]
的對應函數,會產生 List[List[B]]
。為了避免管理清單清單,我們可能想要將值「扁平化」成單一清單。
這就是 Monad
的用武之地。類型 F[_]
的 Monad
是 Functor[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`
由於 Monad
是 Functor
的子類型,因此 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,它作用於函式而非資料類型,例如 List
或 Option
。它可用於結合多個需要相同參數的函式。例如,需要存取某些組態、內容、環境變數等的多個函式。
讓我們定義一個 Config
類型和兩個使用它的函式
trait Config
// ...
def compute(i: Int)(config: Config): String = ???
def show(str: String)(config: Config): Unit = ???
我們可能想要將 compute
和 show
結合成一個單一函式,接受 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)允許簡潔且自然地表達類型類別。