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:
Int
to my type RichComparisonInt
- didn't help because of the evaluation way pictured above,Int
with my class - Cannot be done, because Int
is both abstract
and final
,case class
with name <
, just like ::
, but then I've find out, that this class is created just for pattern matching,=> 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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With