在 GitHub 上編輯此頁面

編譯時操作

scala.compiletime 套件

scala.compiletime 套件包含提供對值執行編譯時間運算的支援的輔助定義。以下說明這些定義。

constValueconstValueOpt

constValue 是產生型別所表示的常數值的函數,或者如果型別不是常數型別,則會產生編譯時間錯誤。

import scala.compiletime.constValue
import scala.compiletime.ops.int.S

transparent inline def toIntC[N]: Int =
  inline constValue[N] match
    case 0        => 0
    case _: S[n1] => 1 + toIntC[n1]

inline val ctwo = toIntC[2]

constValueOptconstValue 相同,但會傳回 Option[T],使我們能夠處理值不存在的情況。請注意,S 是某個單例型別的後繼型別的型別。例如,型別 S[1] 是單例型別 2

由於元組不是常數型別,即使其組成部分是,因此有 constValueTuple,它會針對元組型別 (X1, ..., Xn) 傳回元組值 (constValue[X1], ..., constValue[Xn])

erasedValue

到目前為止,我們已經看到內聯方法將術語(元組和整數)作為參數。如果我們想根據類型來區分基本情況呢?例如,我們希望能夠編寫一個函數 defaultValue,給定一個類型 T,如果存在,則選擇性地返回 T 的預設值。我們已經可以使用重寫匹配表達式和一個簡單的輔助函數 scala.compiletime.erasedValue 來表達這一點,其定義如下

def erasedValue[T]: T

erasedValue 函數假裝返回其類型參數 T 的值。呼叫此函數將永遠導致編譯時期錯誤,除非在內聯時從程式碼中移除呼叫。

使用 erasedValue,我們可以將 defaultValue 定義如下

import scala.compiletime.erasedValue

transparent inline def defaultValue[T] =
  inline erasedValue[T] match
    case _: Byte    => Some(0: Byte)
    case _: Char    => Some(0: Char)
    case _: Short   => Some(0: Short)
    case _: Int     => Some(0)
    case _: Long    => Some(0L)
    case _: Float   => Some(0.0f)
    case _: Double  => Some(0.0d)
    case _: Boolean => Some(false)
    case _: Unit    => Some(())
    case _          => None

然後

val dInt: Some[Int] = defaultValue[Int]
val dDouble: Some[Double] = defaultValue[Double]
val dBoolean: Some[Boolean] = defaultValue[Boolean]
val dAny: None.type = defaultValue[Any]

作為另一個範例,考慮以下 toInt 的類型層級版本:給定表示皮亞諾數的類型,傳回對應的整數。考慮如上文內聯匹配區段中的數字定義。以下是 toIntT 的定義方式

transparent inline def toIntT[N <: Nat]: Int =
  inline scala.compiletime.erasedValue[N] match
    case _: Zero.type => 0
    case _: Succ[n] => toIntT[n] + 1

inline val two = toIntT[Succ[Succ[Zero.type]]]

erasedValueerased 方法,因此無法使用且沒有執行時期行為。由於 toIntTN 的靜態類型執行靜態檢查,因此我們可以安全地使用它來檢查其回傳類型(在本例中為 S[S[Z]])。

error

error 方法用於在內聯擴充期間產生使用者定義的編譯錯誤。它具有以下簽章

inline def error(inline msg: String): Nothing

如果內聯擴充導致呼叫 error(msgStr),編譯器會產生包含給定 msgStr 的錯誤訊息。

import scala.compiletime.{error, codeOf}

inline def fail() =
  error("failed for a reason")

fail() // error: failed for a reason

inline def fail(inline p1: Any) =
  error("failed on: " + codeOf(p1))

fail(identity("foo")) // error: failed on: identity[String]("foo")

scala.compiletime.ops 套件

scala.compiletime.ops 套件包含提供對單例類型進行基本運算支援的類型。例如,scala.compiletime.ops.int.* 提供對兩個單例 Int 類型進行乘法的支援,而 scala.compiletime.ops.boolean.&& 提供對兩個 Boolean 類型的連接支援。當 scala.compiletime.ops 中類型的所有參數都是單例類型時,編譯器可以評估運算結果。

import scala.compiletime.ops.int.*
import scala.compiletime.ops.boolean.*

val conjunction: true && true = true
val multiplication: 3 * 5 = 15

這些單例操作類型中的許多類型旨在用作中綴(如 SLS §3.2.10 中所示)。

由於類型別名具有與其術語級別等效項相同的優先順序規則,因此這些操作會與預期的優先順序規則組合。

import scala.compiletime.ops.int.*
val x: 1 + 2 * 3 = 7

操作類型位於以左手邊參數類型命名的套件中:例如,scala.compiletime.ops.int.+ 表示兩個數字的加法,而 scala.compiletime.ops.string.+ 表示字串串接。若要同時使用這兩個類型並將它們區分開來,比對類型可以傳送至正確的實作

import scala.compiletime.ops.*

import scala.annotation.infix

type +[X <: Int | String, Y <: Int | String] = (X, Y) match
  case (Int, Int) => int.+[X, Y]
  case (String, String) => string.+[X, Y]

val concat: "a" + "b" = "ab"
val addition: 1 + 1 = 2

有選擇性地傳送給定值

新的 summonFrom 建構函式讓隱式搜尋可在函式內容中使用。若要解決建立正確集合的問題,可以使用下列方式

import scala.compiletime.summonFrom

inline def setFor[T]: Set[T] = summonFrom {
  case ord: Ordering[T] => new TreeSet[T]()(using ord)
  case _                => new HashSet[T]
}

summonFrom 呼叫會將模式比對封閉作為引數。封閉中的所有模式都是 識別碼:類型 形式的類型歸屬。

模式會依序嘗試。第一個模式為 x: T 的情況,其中可以傳送類型為 T 的內容值。

或者,也可以使用模式約束的給定值實例,這可以避免使用明確的 using 子句。例如,setFor 也可以表述如下

import scala.compiletime.summonFrom

inline def setFor[T]: Set[T] = summonFrom {
  case given Ordering[T] => new TreeSet[T]
  case _                 => new HashSet[T]
}

summonFrom 應用程式必須在編譯時縮減。

因此,如果給定值實例 Ordering[String] 在隱式範圍內,則上述程式碼會傳回 TreeSet[String] 的新實例。此類實例定義在 Ordering 的伴隨物件中,因此永遠只會有一個。

summon[Ordering[String]] // Proves that an Ordering[String] is in scope

println(setFor[String].getClass) // prints class scala.collection.immutable.TreeSet

注意: summonFrom 應用程式可能會引發歧義錯誤。考慮以下程式碼,其中兩個給定值在 A 類型的範圍內。如果套用 f,則 f 中的模式比對會引發歧義錯誤。

class A
given a1: A = new A
given a2: A = new A

inline def f: Any = summonFrom {
  case given _: A => ???  // error: ambiguous givens
}

summonInline

簡寫 summonInline 提供一種簡單的方法來撰寫 summon,此方法會延遲到呼叫內聯時才執行。與 summonFrom 不同,如果找不到召喚類型的特定執行個體,summonInline 也會產生隱式找不到的錯誤。

import scala.compiletime.summonInline
import scala.annotation.implicitNotFound

@implicitNotFound("Missing One")
trait Missing1

@implicitNotFound("Missing Two")
trait Missing2

trait NotMissing
given NotMissing = ???

transparent inline def summonInlineCheck[T <: Int](inline t : T) : Any =
  inline t match
    case 1 => summonInline[Missing1]
    case 2 => summonInline[Missing2]
    case _ => summonInline[NotMissing]

val missing1 = summonInlineCheck(1) // error: Missing One
val missing2 = summonInlineCheck(2) // error: Missing Two
val notMissing : NotMissing = summonInlineCheck(3)

參考

如需有關編譯時期運算的更多資訊,請參閱 PR #4768,其中說明如何將 summonFrom 的前身(隱式比對)用於類型層次程式設計和程式碼專門化,以及 PR #7201,其中說明新的 summonFrom 語法。