5

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?

Ben Reich
  • 16,222
  • 2
  • 38
  • 59
Bartek Andrzejczak
  • 1,292
  • 2
  • 14
  • 27
  • `implicit class` for `Boolean` with a `<(y: Int)` method, which is a macro, and inspects its `prefix`, deconstructs it as `x.y(a)`, and there you go. – sjrd Apr 23 '15 at 15:36
  • 1
    @sjrd I don't think that's going to work in the general case because of folding. – Travis Brown Apr 23 '15 at 15:42
  • 1
    Folding only happens if `a` is a `final val` without type. In the above example, I can guarantee that the compiler doesn't fold it (at least not during typer, before macros kick in). – sjrd Apr 23 '15 at 20:22

2 Answers2

3

You'll have trouble naming the method < without using a macro, since the compiler will always choose the < method that is actually on Int, as opposed to any enriched class. But if you can give that up, you can enrich Int as you suggested, and return an intermediary type that keeps track of the comparison thus 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 could also add an implicit conversion from Comparison to Boolean so that you can use <<< for half a comparison as well:

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

Which would allow you to do:

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

Now once you've introduced this implicit conversion, you can go back and make <<< on Comparison return Comparison instead, allowing you to do even more extended chaining:

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
Ben Reich
  • 16,222
  • 2
  • 38
  • 59
  • This is a nice solution, but it's an easy one. The primary goal of my question was not to allow such comparisons. It was more about the lone issue of expressions compilation and expression structure extraction. – Bartek Andrzejczak Apr 23 '15 at 15:51
  • 1
    @BartekAndrzejczak Ah I see. Well I'll leave this here in case it suits somebody else's needs. Macros are definitely the way to go here in your case, then. I'll take a look if I get a chance later and nobody else gets to it. – Ben Reich Apr 23 '15 at 15:55
3

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.

Community
  • 1
  • 1
Ben Reich
  • 16,222
  • 2
  • 38
  • 59