编译时操作
scala.compiletime
包
scala.compiletime
包包含一些帮助程序定义,这些定义为值提供编译时操作的支持。它们在以下内容中进行了描述。
constValue
和 constValueOpt
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]
constValueOpt
与 constValue
相同,但返回一个 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
方法,因此无法使用且没有运行时行为。由于 toIntT
对 N
的静态类型执行静态检查,因此我们可以安全地使用它来检查其返回类型(在本例中为 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
的给定。如果应用 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
语法。