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?
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?
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'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 Int
s or Double
s:
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