在 GitHub 上編輯此頁面

已刪除定義

erased 是一個修飾詞,表示某些定義或表達式會被編譯器刪除,而不是顯示在編譯輸出中。它還不是 Scala 語言標準的一部分。若要啟用 erased,請開啟語言功能 experimental.erasedDefinitions。這可以使用語言匯入來完成

import scala.language.experimental.erasedDefinitions

或設定命令列選項 -language:experimental.erasedDefinitions。已刪除的定義必須在實驗範圍內(請參閱 實驗定義)。

為什麼要刪除條款?

我們用一個範例來描述刪除條款背後的動機。以下我們展示一個簡單的狀態機,它可以處於狀態 OnOff。機器只能在目前為 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

不僅參數可以標記為已清除,valdef 也可以標記為 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)

帶有已清除證據的狀態機範例

以下範例是簡單狀態機的延伸實作,它可以處於 OnOff 狀態。機器只能在目前為 Off 的情況下,使用 turnedOn 將狀態從 Off 變更為 On;反之,只能在目前為 On 的情況下,使用 turnedOff 將狀態從 On 變更為 Off。這些最後的約束會透過 IsOff[S]IsOn[S] 擷取,給定的證據僅存在於 IsOff[Off]IsOn[On]。例如,不允許在 Off 狀態下呼叫 turnedOff,因為我們需要一個證據 IsOn[Off],而找不到這個證據。

由於在這些函數的主體中未曾使用給定的 turnedOnturnedOff 證據,因此我們可以將它們標記為 erased。這將在執行階段移除證據參數,但我們仍會評估作為參數找到的 isOnisOff 給定值。由於 isOnisOff 僅作為 erased 參數使用,因此我們可以將它們標記為 erased,從而移除對 isOnisOff 證據的評估。

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 的參照。

更多詳細資料