Scala - How to "Delay" Compilation of an Expression

I meant to implement chained comparison operators for Scala, but after several attempts, I don't think there is a way to do this. Here's how it should work:

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

The problem is that the scala compiler is rather greedy for evaluating expressions, so the above expressions are evaluated as follows:

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

I tried writing code to somehow implement it, and here is what I tried:

  • Implicit conversions from type Int to my type RichComparisonInt - did not help due to the evaluation method shown above,
  • Overriding the Int class with my class - cannot be done because Int is abstract and final ,
  • I tried to create a case class named < , just like :: , but then I found out that this class was created only to match the pattern,
  • I mean creating an implicit conversion from => Boolean that will work at the compilation level, but there is no way to extract the operation parameters, which led to the result of Boolean .

Is there a way to do this in Scala? Maybe macros could do the job?

+5
source share
2 answers

It uses a solution using macros . The general approach here is to enrich Boolean so that it has a macro method that looks at the prefix context to find the comparison that was used to create this Boolean .

For example, suppose we have:

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

And the macro definition with the method header:

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

Now suppose the compiler parses the expression 1 < 2 < 3 . Apparently, we could use c.prefix to get the expression 1 < 2 when evaluating the body of a macro object. However, the concept of permanent folding prevents us from doing this here. Constant folding is the process by which the compiler calculates the given constants at compile time. Thus, by the time macros are evaluated, c.prefix already complex as true in this case. We lost the expression 1 < 2 , which led to true . You can learn more about the permanent crease and their interactions with Scala macros on this issue and a little about this question .

If we limit the scope of discussion only to expressions of the form C1 < x < C2 , where C1 and C2 are constants, and x is a variable, then this becomes feasible, since this type is not affected by constant bending. Here is the 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 map the prefix context to the expected type, extract the corresponding parts ( lhs and x ), create new subtrees using c.Expr[Int] and create a new complete expression tree using reify and splice to make the desired three-way comparison. If the match with the expected type does not match, this will not compile.

This allows us to do:

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

Optional!

macro documents , trees , and this presentation are good resources to learn more about macros.

+3
source

You will have a problem with the method name < without using a macro, because the compiler will always select the method < , which is actually on Int , unlike any enriched class. But if you can do this, you can enrich Int , as you said, and return an intermediate type that keeps track of the comparison so far:

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

Then we can do:

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

If you want, you can also add an implicit conversion from Comparison to Boolean so you can use <<< for half the comparison:

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

What would you do:

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

Now that you have entered this implicit conversion, you can go back and do <<< on Comparison return Comparison instead, which allows you to do an even more extended chain:

 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 
+3
source

All Articles