确定表达式的值在编译时是否已知

Determine whether an expression's value is known at compile time

假设我想创建一个 NonZero 类型,这样我的整数除法函数是总计:

def div(numerator: Int, denominator: NonZero): Int =
  numerator / denominator.value

我可以通过使用私有构造函数创建 NonZero class 来实现:

class NonZero private[NonZero] (val value : Int) { /*...*/ }

还有一个辅助对象,用于保存 Int => Option[NonZero] 构造函数和一个 unapply,因此它可以在 match 表达式中使用:

object NonZero {
  def build(n:Int): Option[NonZero] = n match {
    case 0 => None
    case n => Some(new NonZero(n))
  }
  def unapply(nz: NonZero): Option[Int] = Some(nz.value)
  // ...
}

build 对于运行时值来说很好,但是必须对文字执行 NonZero.build(3).get 感觉很难看。

使用宏,我们可以定义 apply only for literals,因此 NonZero(3) 有效,但 NonZero(0) 是编译时错误:

object NonZero {
  // ...
  def apply(n: Int): NonZero = macro apply_impl
  def apply_impl(c: Context)(n: c.Expr[Int]): c.Expr[NonZero] = {
    import c.universe._
    n match {
      case Expr(Literal(Constant(nValue: Int))) if nValue != 0 =>
        c.Expr(q"NonZero.build(n).get")
      case _ => throw new IllegalArgumentException("Expected non-zero integer literal")
    }
  }
}

但是这个宏没有它应有的用处,因为它只允许文字,而不是编译时常量表达式:

final val X: Int = 3
NonZero(X) // compile-time error

我在我的宏中 could pattern match on Expr(Constant(_)),但是 NonZero(X + 1) 呢?我宁愿不必实现自己的 Scala 表达式求值器。

是否有帮助程序或一些简单的方法来确定在编译时赋予宏的表达式的值是否已知(C++ 会调用什么 constexpr)?

如果忽略宏,那么在Scala中,编译时只存在类型,运行时只存在值。您可以使用类型级别的技巧在编译时将数字编码为类型,例如

这是上述 Peano 算术示例的简化版本。首先,我们定义一个类型类,它展示了一些类型如何转换为整数。

@annotation.implicitNotFound("Create an implicit of type TValue[${T}] to convert ${T} values to integers.")
final class TValue[T](val get: Int) extends AnyVal

然后,我们定义 Peano 'zero' 类型并展示它如何转换为运行时整数 0:

case object TZero {
  implicit val tValue: TValue[TZero.type] = new TValue(0)
}

然后是 Peano 'successor' 类型以及它如何转换为运行时整数 1 + 先前的值:

case class TSucc[T: TValue]()
object TSucc {
  implicit def tValue[TPrev](implicit prevTValue: TValue[TPrev]): TValue[TSucc[TPrev]] =
    new TValue(1 + prevTValue.get)
}

然后测试安全分区:

object Test {
  def safeDiv[T](numerator: Int, denominator: TSucc[T])(implicit tValue: TValue[TSucc[T]]): Int =
    numerator / tValue.get
}

尝试一下:

scala> Test.safeDiv(10, TZero)
<console>:14: error: type mismatch;
 found   : TZero.type
 required: TSucc[?]
       Test.safeDiv(10, TZero)
                        ^

scala> Test.safeDiv(10, TSucc[String]())
<console>:14: error: Create an implicit of type TValue[String] to convert String values to integers.
       Test.safeDiv(10, TSucc[String]())
                                     ^

scala> Test.safeDiv(10, TSucc[TZero.type]) // 10/1
res2: Int = 10

scala> Test.safeDiv(10, TSucc[TSucc[TZero.type]]) // 10/2
res3: Int = 5

尽管您可以想象,这很快就会变得冗长。

led me to Context.eval,我一直想要的帮手:

object NonZero {
  // ...
  def apply(n: Int): NonZero = macro apply_impl
  def apply_impl(c: Context)(n: c.Expr[Int]): c.Expr[NonZero] = try {
    if (c.eval(n) != 0) {
      import c.universe._
      c.Expr(q"NonZero.build(n).get")
    } else {
      throw new IllegalArgumentException("Non-zero value required")
    }
  } catch {
    case _: scala.tools.reflect.ToolBoxError =>
      throw new IllegalArgumentException("Unable to evaluate " + n.tree + " at compile time")
  }
}

所以现在我可以传递 NonZero.apply 常量和由常量组成的表达式:

scala> final val N = 3
scala> NonZero(N)
res0: NonZero = NonZero(3)
scala> NonZero(2*N + 1)
res1: NonZero = NonZero(7)
scala> NonZero(N - 3)
IllegalArgumentException: ...
scala> NonZero((n:Int) => 2*n + 1)(3))
IllegalArgumentException: ...

如果 eval 可以像上面最后一个例子一样处理纯函数就好了,这已经足够了。

令人尴尬的是,回顾并重新测试我之前的问题代码证明我的原始宏也能处理相同的表达式!

我断言 final val X = 3; NonZero(X) // compile-time error 是错误的,因为所有的计算都是通过内联处理的(正如 som-snytt 的评论所暗示的那样)。