已删除的定义
erased
是一个修饰符,表示某些定义或表达式被编译器擦除,而不是在编译后的输出中表示。它还不是 Scala 语言标准的一部分。要启用 erased
,请打开语言功能 experimental.erasedDefinitions
。这可以通过语言导入来完成
import scala.language.experimental.erasedDefinitions
或通过设置命令行选项 -language:experimental.erasedDefinitions
来完成。已擦除的定义必须在实验性范围内(参见 实验性定义)。
为什么擦除项?
让我们用一个例子来描述擦除项背后的动机。在下面,我们展示了一个简单的状态机,它可以处于 On
或 Off
状态。该机器只能在当前处于 Off
状态的情况下,通过 turnedOn
从 Off
状态变为 On
状态。最后一个约束通过 IsOff[S]
上下文证据来捕获,该证据仅存在于 IsOff[Off]
中。例如,不允许在 On
状态下调用 turnedOn
,因为我们需要一个类型为 IsOff[On]
的证据,而该证据将找不到。
sealed trait State
final class On extends State
final class Off extends State
@implicitNotFound("State must be Off")
class IsOff[S <: State]
object IsOff:
given isOff: IsOff[Off] = new IsOff[Off]
class Machine[S <: State]:
def turnedOn(using IsOff[S]): Machine[On] = new Machine[On]
val m = new Machine[Off]
m.turnedOn
m.turnedOn.turnedOn // ERROR
// ^
// State must be Off
请注意,在上面的代码中,IsOff
的实际上下文参数在运行时从未使用过;它们仅用于在编译时建立正确的约束。由于这些术语在运行时从未使用过,因此没有真正需要保留它们,但它们仍然需要以某种形式存在于生成的代码中,以便能够进行单独编译并保留二进制兼容性。我们引入 *擦除项* 来克服此限制:我们能够在编译时对项强制执行正确的约束。这些项没有运行时语义,并且它们被完全擦除。
如何定义擦除项?
方法和函数的参数可以声明为擦除的,在每个擦除参数前面放置 erased
(类似于 inline
)。
def methodWithErasedEv(erased ev: Ev, x: Int): Int = x + 2
val lambdaWithErasedEv: (erased Ev, Int) => Int =
(erased ev, x) => x + 2
erased
参数不能用于计算,但可以作为其他 erased
参数的参数使用。
def methodWithErasedInt1(erased i: Int): Int =
i + 42 // ERROR: can not use i
def methodWithErasedInt2(erased i: Int): Int =
methodWithErasedInt1(i) // OK
不仅参数可以标记为擦除的,val
和 def
也可以标记为 erased
。这些也只可作为 erased
参数的参数使用。
erased val erasedEvidence: Ev = ...
methodWithErasedEv(erasedEvidence, 40) // 42
擦除值在运行时会发生什么?
由于 erased
保证不会在计算中使用,因此它们可以并且将被擦除。
// becomes def methodWithErasedEv(x: Int): Int at runtime
def methodWithErasedEv(x: Int, erased ev: Ev): Int = ...
def evidence1: Ev = ...
erased def erasedEvidence2: Ev = ... // does not exist at runtime
erased val erasedEvidence3: Ev = ... // does not exist at runtime
// evidence1 is not evaluated and only `x` is passed to methodWithErasedEv
methodWithErasedEv(x, evidence1)
带有擦除证据的状态机示例
以下示例是简单状态机的扩展实现,该状态机可以处于 On
或 Off
状态。机器只能在当前处于 Off
状态时使用 turnedOn
从 Off
状态变为 On
状态,反之,只能在当前处于 On
状态时使用 turnedOff
从 On
状态变为 Off
状态。这些最后的约束通过 IsOff[S]
和 IsOn[S]
来捕获,给定的证据仅存在于 IsOff[Off]
和 IsOn[On]
中。例如,不允许在 Off
状态下调用 turnedOff
,因为我们需要一个 IsOn[Off]
证据,而该证据将找不到。
由于 turnedOn
和 turnedOff
的给定证据未在这些函数的主体中使用,因此我们可以将它们标记为 erased
。这将在运行时删除证据参数,但我们仍然会评估作为参数找到的 isOn
和 isOff
给定值。由于 isOn
和 isOff
除了作为 erased
参数之外没有其他用途,因此我们可以将它们标记为 erased
,从而删除 isOn
和 isOff
证据的评估。
import scala.annotation.implicitNotFound
sealed trait State
final class On extends State
final class Off extends State
@implicitNotFound("State must be Off")
class IsOff[S <: State]
object IsOff:
// will not be called at runtime for turnedOn, the
// compiler will only require that this evidence exists
given IsOff[Off] = new IsOff[Off]
@implicitNotFound("State must be On")
class IsOn[S <: State]
object IsOn:
// will not exist at runtime, the compiler will only
// require that this evidence exists at compile time
erased given IsOn[On] = new IsOn[On]
class Machine[S <: State] private ():
// ev will disappear from both functions
def turnedOn(using erased ev: IsOff[S]): Machine[On] = new Machine[On]
def turnedOff(using erased ev: IsOn[S]): Machine[Off] = new Machine[Off]
object Machine:
def newMachine(): Machine[Off] = new Machine[Off]
@main def test =
val m = Machine.newMachine()
m.turnedOn
m.turnedOn.turnedOff
// m.turnedOff
// ^
// State must be On
// m.turnedOn.turnedOn
// ^
// State must be Off
请注意,在 编译时操作 中,我们讨论了 erasedValue
和内联匹配。erasedValue
在内部使用 erased
实现(并且不是实验性的),因此上面的状态机可以编码如下
import scala.compiletime.*
sealed trait State
final class On extends State
final class Off extends State
class Machine[S <: State]:
transparent inline def turnOn(): Machine[On] =
inline erasedValue[S] match
case _: Off => new Machine[On]
case _: On => error("Turning on an already turned on machine")
transparent inline def turnOff(): Machine[Off] =
inline erasedValue[S] match
case _: On => new Machine[Off]
case _: Off => error("Turning off an already turned off machine")
object Machine:
def newMachine(): Machine[Off] =
println("newMachine")
new Machine[Off]
end Machine
@main def test =
val m = Machine.newMachine()
m.turnOn()
m.turnOn().turnOff()
m.turnOn().turnOn() // error: Turning on an already turned on machine
擦除类
erased
也可以用作类的修饰符。擦除类仅用于擦除定义。如果 val 定义或参数的类型是(可能被别名化、细化或实例化的)擦除类,则该定义被认为是 erased
本身。类似地,具有擦除类返回值类型的方法被认为是 erased
本身。由于给定实例扩展为 vals 和 defs,因此如果它们生成的类型是擦除类,则它们也被认为是擦除的。最后,具有擦除类作为参数的函数类型将变成擦除函数类型。
示例
erased class CanRead
val x: CanRead = ... // `x` is turned into an erased val
val y: CanRead => Int = ... // the function is turned into an erased function
def f(x: CanRead) = ... // `f` takes an erased parameter
def g(): CanRead = ... // `g` is turned into an erased def
given CanRead = ... // the anonymous given is assumed to be erased
上面的代码扩展为
erased class CanRead
erased val x: CanRead = ...
val y: (erased CanRead) => Int = ...
def f(erased x: CanRead) = ...
erased def g(): CanRead = ...
erased given CanRead = ...
在擦除后,它会检查是否没有对擦除类值的引用,并且没有创建擦除类的实例。因此,以下将是错误的
val err: Any = CanRead() // error: illegal reference to erased class CanRead
这里,err
的类型是 Any
,因此 err
不被认为是擦除的。然而,它的初始化值是对擦除类 CanRead
的引用。