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 的地方使用 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 这样的基本类型将没有装箱开销

不透明类型的摘要

不透明类型提供对实现细节的健全抽象,而不会造成性能开销。如上所述,不透明类型使用方便,并且与 扩展方法 特性很好地集成在一起。

此页面的贡献者