Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala - How to "delay" expression's compilation

I've been meaning to implement a chained comparison operators for Scala, but after few tries I don't think there is a way to do it. This is how it's supposed to work:

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

The problem is, scala compiler is pretty greedy while evaluating expressions, so the expressions above are evaluated as follows:

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

I've tried to write the code to implement it somehow and here is what I've tried:

  • Implicit conversions from type Int to my type RichComparisonInt - didn't help because of the evaluation way pictured above,
  • Overriding class Int with my class - Cannot be done, because Int is both abstract and final,
  • I've tried creating case class with name <, just like ::, but then I've find out, that this class is created just for pattern matching,
  • I've though about creating implicit conversion from => Boolean, which would work on the compilation level, but there's no way to extract parameters of the operation, that led to Boolean result.

Is there any way to do it in Scala? Maybe macros could've done the job?

like image 717
Bartek Andrzejczak Avatar asked Apr 23 '15 15:04

Bartek Andrzejczak


1 Answers

Here's a solution that uses macros. The general approach here is to enrich Boolean so that it has a macro method that looks at the prefix of the context to find the comparison that was used to generate that Boolean.

For example, suppose we have:

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

And a macro definition with method header:

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

Now suppose that the compiler is parsing the expression 1 < 2 < 3. We could apparently use c.prefix to get at the expression 1 < 2 while evaluating the macro method body. However, the concept of 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.

If we can limit the scope of the discussion to only expressions of the form C1 < x < C2, where C1 and C2 are constants, and x is a variable, then this becomes doable, since this type of expression won't be affected by constant folding. Here is an implementation:

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.")
    }
  } 
}

Here, we match the context prefix to the expected type, extract the relevant parts (lhs and x), construct new subtrees using c.Expr[Int], and construct a new full expression tree using reify and splice to make the desired 3-way comparison. If there is no match with the expected type, this will fail to compile.

This allows us to do:

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

As desired!

The docs about macros, trees, and this presentation are good resources to learn more about macros.

like image 137
Ben Reich Avatar answered Nov 05 '22 05:11

Ben Reich