Scala - 如何 "delay" 表达式的编译

Scala - How to "delay" expression's compilation

我一直想为 Scala 实现链式比较运算符,但经过几次尝试,我认为没有办法做到这一点。它应该是这样工作的:

val a = 3
1 < a < 5 //yields true
3 < a < 5 //yields false

问题是,scala 编译器在计算表达式时非常贪婪,所以上面的表达式计算如下:

1 < a    //yields true
true < 5 //compilation error

我尝试编写代码来以某种方式实现它,这是我尝试过的:

在 Scala 中有什么方法可以做到吗?也许宏可以完成这项工作?

如果不使用宏,您将无法命名方法 <,因为编译器将始终选择实际在 Int 上的 < 方法,而不是任何方法丰富 class。但是,如果您可以放弃它,则可以按照您的建议丰富 Int,并且 return 是一种中间类型,可以跟踪到目前为止的比较:

implicit class RichIntComparison(val x: Int) extends AnyVal {
  def <<<(y: Int) = Comparison(x < y, y)
}

case class Comparison(soFar: Boolean, y: Int) {
  def <<<(z: Int) = soFar && (y < z)
}

那么我们可以这样做:

1 <<< 2 <<< 3

//Equivalent to:
val rc: Comparison = RichIntComparison(1).<<<(2)
rc.<<<(3)

如果需要,您还可以添加从 ComparisonBoolean 的隐式转换,以便您也可以使用 <<< 进行半比较:

object Comparison {
  implicit def comparisonToBoolean(c: Comparison): Boolean = c.soFar
}

您可以这样做:

val comp1: Boolean = 1 <<< 2 //true
val comp2: Boolean = 1 <<< 2 <<< 3 //true 

现在,一旦您引入了这种隐式转换,您就可以返回并在 Comparison return Comparison 上创建 <<<,从而允许您做更多事情扩展链接:

case class Comparison(soFar: Boolean, y: Int) {
  def <<<(z: Int): Comparison = Comparison(soFar && (y < z), z)
  //You can also use < for everything after the first comparison:
  def <(z: Int) = <<<(z)
}

//Now, we can chain:
val x: Boolean = 1 <<< 2 <<< 3 <<< 4 <<< 3 //false
val x: Boolean = 1 <<< 2 < 3 < 4 < 7 //true

这是一个使用 macros 的解决方案。这里的一般方法是丰富 Boolean 以便它有一个 macro 方法查看上下文的 prefix 以找到用于生成 Boolean 的比较.

例如,假设我们有:

implicit class RichBooleanComparison(val x: Boolean) extends AnyVal {
  def <(rightConstant: Int): Boolean = macro Compare.ltImpl
}

还有一个 macro 带有方法头的定义:

def ltImpl(c: Context)(rightConstant: c.Expr[Int]): c.Expr[Boolean]

现在假设编译器正在解析表达式 1 < 2 < 3。在评估宏方法体时,我们显然可以使用 c.prefix 来获取表达式 1 < 2。但是,constant folding prevents us from doing so here. Constant folding is the process by which the compiler computes predetermined constants at compile time. So by the time macros are being evaluated, the c.prefix has already been folded to be just true in this case. We have lost the 1 < 2 expression that led to true. You can read more about constant folding and their interactions with Scala macros on this issue and a little bit on this question的概念。

如果我们可以将讨论范围限制在 C1 < x < C2 形式的表达式,其中 C1C2 是常量,而 x 是变量,那么这就变得可行了,因为这种类型的表达式不会受到常量折叠的影响。这是一个实现:

object Compare {
  def ltImpl(c: Context)(rightConstant: c.Expr[Int]): c.Expr[Boolean] = {
    import c.universe._
    c.prefix.tree match {
      case Apply(_, Apply(Select(lhs@Literal(Constant(_)), _), (x@Select(_, TermName(_))) :: Nil) :: Nil) => 
          val leftConstant = c.Expr[Int](lhs)
          val variable = c.Expr[Int](x)
          reify((leftConstant.splice < variable.splice) && (variable.splice < rightConstant.splice))

      case _ => c.abort(c.enclosingPosition, s"Invalid format.  Must have format c1<x<c2, where c1 and c2 are constants, and x is variable.")
    }
  } 
}

在这里,我们将上下文 prefix 匹配到预期类型,提取相关部分(lhsx),使用 c.Expr[Int] 构造新的子树,以及使用 reifysplice 构建一个新的完整表达式树,以进行所需的三向比较。如果与预期类型不匹配,将无法编译。

这让我们可以做到:

val x = 5
1 < x < 5 //true
6 < x < 7 //false
3 < x < 4 //false

随心所欲!

docs about macros, trees, and this presentation 是学习更多宏的好资源。