tags:

views:

197

answers:

2

Python supports an elegant syntax for "chained comparisons", for example:

0 <= n < 256

meaning,

0 <= n and n < 256

Knowing that it's a fairly flexible language syntactically, is it possible to emulate this feature in Scala?

+4  A: 

Not really. One have to remember that, aside from a few keywords, everything in Scala is a method invokation on an object.

So we need to call the "<=" and "<" methods on an object, and each such method receives a parameter. You need, then, four objects, and there are three explicit ones, and two implicit ones -- the results of each method.

You could, theoretically, feed the result of the one of the methods to another method. I can think of two ways of doing that:

  • Having <= return an object that has both the parameter it received and the result of the comparision, and having < use both these values as appropriate.

  • Having <= return either false or the parameter it received, and having < either fail or compare against the other parameter. This can be done with the Either class, or something based upon it.

These two solutions are very similar, in fact.

One problem is that consumers of such comparision operators expect Boolean as a result. That is actually the easiest thing to solve, as you could define an implicit from Either[Boolean,T] into Boolean.

So, theoretically, that is possible. You could do it with a class of your own. But how would you go about changing the existing methods already defined? The famous Pimp My Class pattern is used to add behavior, not to change it.

Here is an implementation of the second option:

object ChainedBooleans {

  case class MyBoolean(flag: Either[Boolean, MyInt]) {
    def &&(other: MyBoolean): Either[Boolean, MyInt] = 
      if (flag.isRight || flag.left.get) other.flag else Left(false)

    def <(other: MyInt): Either[Boolean, MyInt] = 
      if (flag.isRight || flag.left.get) flag.right.get < other else Left(false) 
    def >(other: MyInt): Either[Boolean, MyInt] = 
      if (flag.isRight || flag.left.get) flag.right.get > other else Left(false) 
    def ==(other: MyInt): Either[Boolean, MyInt] = 
      if (flag.isRight || flag.left.get) flag.right.get == other else Left(false) 
    def !=(other: MyInt): Either[Boolean, MyInt] = 
      if (flag.isRight || flag.left.get) flag.right.get != other else Left(false) 
    def <=(other: MyInt): Either[Boolean, MyInt] = 
      if (flag.isRight || flag.left.get) flag.right.get <= other else Left(false) 
    def >=(other: MyInt): Either[Boolean, MyInt] = 
      if (flag.isRight || flag.left.get) flag.right.get >= other else Left(false) 
  }

  implicit def toMyBoolean(flag: Either[Boolean, MyInt]) = new MyBoolean(flag)
  implicit def toBoolean(flag: Either[Boolean, MyInt]) = 
    flag.isRight || flag.left.get 

  case class MyInt(n: Int) {
    def <(other: MyInt): Either[Boolean, MyInt] =
      if (n < other.n) Right(other) else Left(false)

    def ==(other: MyInt): Either[Boolean, MyInt] =
      if (n == other.n) Right(other) else Left(false)

    def !=(other: MyInt): Either[Boolean, MyInt] =
      if (n != other.n) Right(other) else Left(false)

    def <=(other: MyInt): Either[Boolean, MyInt] = 
      if (this < other || this == other) Right(other) else Left(false)

    def >(other: MyInt): Either[Boolean, MyInt] =
      if (n > other.n) Right(other) else Left(false)

    def >=(other: MyInt): Either[Boolean, MyInt] = 
      if (this > other || this == other) Right(other) else Left(false)
  }

  implicit def toMyInt(n: Int) = MyInt(n)
}

And here is a session using it, showing what can and what can't be done:

scala> import ChainedBooleans._
import ChainedBooleans._

scala> 2 < 5 < 7
<console>:14: error: no implicit argument matching parameter type Ordering[Any] was found.
       2 < 5 < 7
         ^

scala> 2 < MyInt(5) < 7
res15: Either[Boolean,ChainedBooleans.MyInt] = Right(MyInt(7))

scala> 2 <= MyInt(5) < 7
res16: Either[Boolean,ChainedBooleans.MyInt] = Right(MyInt(7))

scala> 2 <= 5 < MyInt(7)
<console>:14: error: no implicit argument matching parameter type Ordering[ScalaObject] was found.
       2 <= 5 < MyInt(7)
         ^

scala> MyInt(2) < 5 < 7
res18: Either[Boolean,ChainedBooleans.MyInt] = Right(MyInt(7))

scala> MyInt(2) <= 5 < 7
res19: Either[Boolean,ChainedBooleans.MyInt] = Right(MyInt(7))

scala> MyInt(2) <= 1 < 7
res20: Either[Boolean,ChainedBooleans.MyInt] = Left(false)

scala> MyInt(2) <= 7 < 7
res21: Either[Boolean,ChainedBooleans.MyInt] = Left(false)

scala> if (2 <= MyInt(5) < 7) println("It works!") else println("Ow, shucks!")
It works!
Daniel
+5  A: 

Daniel's answer is comprehensive, it's actually hard to add anything to it. As his answer presents one of the choices he mentioned, I'd like just to add my 2 cents and present a very short solution to the problem in the other way. The Daniel's description:

You could, theoretically, feed the result of the one of the methods to another method. I can think of two ways of doing that:

  • Having <= return an object that has both the parameter it received and the result of the comparision, and having < use both these values as appropriate.

CmpChain will serve as an accumulator of comparisions already made along with a free rightmost object, so that we can compare it to the next:

class CmpChain[T <% Ordered[T]](val left: Boolean, x: T) {
  def <(y: T) = new CmpChain(left && x < y, y)
  def <=(y: T) = new CmpChain(left && x <= y, y)
  // > and >= are analogous

  def asBoolean = left
}

implicit def ordToCmpChain[T <% Ordered[T]](x: T) = new AnyRef {
  def cmp = new CmpChain(true, x)
}
implicit def rToBoolean[T](cc: CmpChain[T]): Boolean = cc.asBoolean

You can use it for any ordered types, like Ints or Doubles:

scala> (1.cmp < 2 < 3 <= 3 < 5).asBoolean                          
res0: Boolean = true

scala> (1.0.cmp < 2).asBoolean
res1: Boolean = true

scala> (2.0.cmp < 2).asBoolean
res2: Boolean = false

Implicit conversion will produce Boolean where it is supposed to be:

scala> val b: Boolean = 1.cmp < 2 < 3 < 3 <= 10  
b: Boolean = false
Alexander Azarov
That's an implementation of the first option I mentioned, and it's very clever at that. Go ahead and copy my explanation, so that your answer can be complete. I much prefer what you came up with than what I had done, and if you complete your answer I'm in favour of giving preference to your answer. But choose something better than "r". :-)
Daniel
Thanks! I've updated the answer.
Alexander Azarov