已刪除定義
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
。由於給定的實例會擴充為 val 和 def,因此如果它們產生的類型是已刪除類別,則假設它們也已刪除。最後,具有已刪除類別作為參數的函式類型會轉換為已刪除函式類型。
範例
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
的參照。