Scala 3 — 書籍

不透明類型

語言
此文件頁面專屬於 Scala 3,並可能涵蓋 Scala 2 中沒有的新概念。除非另有說明,否則此頁面中的所有程式碼範例都假設您使用的是 Scala 3。

不透明類型別名提供類型抽象,沒有任何開銷。在 Scala 2 中,可以使用 值類別 達到類似的結果。

抽象開銷

假設我們想定義一個提供對數字進行算術運算的模組,而數字是以其對數表示。當涉及的數值趨於非常大或接近於零時,這有助於提高精度。

由於區分「常規」雙精度值和以其對數儲存的數字非常重要,因此我們引入了一個類別 Logarithm

class Logarithm(protected val underlying: Double):
  def toDouble: Double = math.exp(underlying)
  def + (that: Logarithm): Logarithm =
    // here we use the apply method on the companion
    Logarithm(this.toDouble + that.toDouble)
  def * (that: Logarithm): Logarithm =
    new Logarithm(this.underlying + that.underlying)

object Logarithm:
  def apply(d: Double): Logarithm = new Logarithm(math.log(d))

伴隨物件上的 apply 方法讓我們可以建立 Logarithm 類型的值,我們可以使用如下方式

val l2 = Logarithm(2.0)
val l3 = Logarithm(3.0)
println((l2 * l3).toDouble) // prints 6.0
println((l2 + l3).toDouble) // prints 4.999...

雖然類別 Logarithm 為以這種特定對數形式儲存的 Double 值提供了一個很好的抽象,但它會造成嚴重的效能開銷:對於每個單一的數學運算,我們都需要擷取底層值,然後再將其包裝在 Logarithm 的新執行個體中。

模組抽象

讓我們考慮另一種方法來實作同一個函式庫。這次我們不將 Logarithm 定義為一個類別,而是使用一個 類型別名 來定義它。首先,我們定義模組的一個抽象介面

trait Logarithms:

  type Logarithm

  // operations on Logarithm
  def add(x: Logarithm, y: Logarithm): Logarithm
  def mul(x: Logarithm, y: Logarithm): Logarithm

  // functions to convert between Double and Logarithm
  def make(d: Double): Logarithm
  def extract(x: Logarithm): Double

  // extension methods to use `add` and `mul` as "methods" on Logarithm
  extension (x: Logarithm)
    def toDouble: Double = extract(x)
    def + (y: Logarithm): Logarithm = add(x, y)
    def * (y: Logarithm): Logarithm = mul(x, y)

現在,讓我們透過說明類型 Logarithm 等於 Double 來實作這個抽象介面

object LogarithmsImpl extends Logarithms:

  type Logarithm = Double

  // operations on Logarithm
  def add(x: Logarithm, y: Logarithm): Logarithm = make(x.toDouble + y.toDouble)
  def mul(x: Logarithm, y: Logarithm): Logarithm = x + y

  // functions to convert between Double and Logarithm
  def make(d: Double): Logarithm = math.log(d)
  def extract(x: Logarithm): Double = math.exp(x)

LogarithmsImpl 的實作中,等式 Logarithm = Double 讓我們可以實作各種方法。

外洩抽象

然而,這種抽象化有點漏洞。我們必須確保針對抽象介面 Logarithms 進行程式設計,且絕不直接使用 LogarithmsImpl。直接使用 LogarithmsImpl 會讓使用者看到等式 Logarithm = Double,而使用者可能會誤用 Double,而預期的是對數雙精度浮點數。例如

import LogarithmsImpl.*
val l: Logarithm = make(1.0)
val d: Double = l // type checks AND leaks the equality!

必須將模組分為抽象介面和實作可能會很有用,但這也需要很多功夫,只是為了隱藏 Logarithm 的實作細節。針對抽象模組 Logarithms 進行程式設計可能會很乏味,而且通常需要使用進階功能,例如路徑相關類型,如下例所示

def someComputation(L: Logarithms)(init: L.Logarithm): L.Logarithm = ...

封裝開銷

類型抽象化,例如 type Logarithm 擦除 至其邊界(在我們的案例中是 Any)。也就是說,雖然我們不需要手動封裝和解封 Double 值,但仍會有一些與封裝基本類型 Double 相關的封裝開銷。

不透明類型

與其手動將 Logarithms 元件拆分成抽象部分和具體實作,我們可以簡單地在 Scala 3 中使用不透明類型來達成類似的效果

object Logarithms:
//vvvvvv this is the important difference!
  opaque type Logarithm = Double

  object Logarithm:
    def apply(d: Double): Logarithm = math.log(d)

  extension (x: Logarithm)
    def toDouble: Double = math.exp(x)
    def + (y: Logarithm): Logarithm = Logarithm(math.exp(x) + math.exp(y))
    def * (y: Logarithm): Logarithm = x + y

事實上,LogarithmDouble 相同,這僅在定義 Logarithm 的範圍內知道,在上述範例中,這對應到物件 Logarithms。類型等式 Logarithm = Double 可用於實作方法(例如 *toDouble)。

然而,在模組之外,類型 Logarithm 完全封裝,或「不透明」。對於 Logarithm 的使用者而言,無法發現 Logarithm 實際上是以 Double 實作

import Logarithms.*
val log2 = Logarithm(2.0)
val log3 = Logarithm(3.0)
println((log2 * log3).toDouble) // prints 6.0
println((log2 + log3).toDouble) // prints 4.999...

val d: Double = log2 // ERROR: Found Logarithm required Double

儘管我們抽象化 Logarithm,但抽象化是免費的:由於只有一個實作,在執行階段,對於 Double 等原始類型,將沒有封裝開銷

不透明類型的摘要

不透明類型提供對實作細節的健全抽象化,而不會造成效能開銷。如上所述,不透明類型使用方便,且與 擴充方法 功能整合得很好。

此頁面的貢獻者