在 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]]]

erasedValue 是一个 erased 方法,因此无法使用且没有运行时行为。由于 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

有选择地召唤 Givens

新的 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 调用采用模式匹配闭包作为参数。闭包中的所有模式都是 identifier : Type 形式的类型归属。

模式按顺序尝试。第一个模式为 x: T 的情况,其中可以召唤类型为 T 的上下文值,将被选中。

或者,还可以使用模式绑定的给定实例,这样可以避免显式使用子句。例如,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 的给定。如果应用 ff 中的模式匹配将引发歧义错误。

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 语法。