Photo by Magda Ehlers from Pexels
Scala doesn't have the traditional ternary operator from Java
// java
var x = condition ? ifTrue : ifFalse
Instead, ternary-like expressions can be defined using if
-else
expressions, which -- unlike in Java -- return a value
// scala
var x = if (condition) ifTrue else ifFalse
(All code from this point on is Scala code.)
But this is a bit verbose. It would be nice if we could somehow recreate the simple ?:
notation in Scala. Can it be done? Let's try.
First, we need to think about what this is actually doing. Basically, a ternary operator defines a function which takes three arguments as parameters -- a boolean condition
and two by-name parameters, one for each possible value of the condition
.
A naive implementation could be a function with a signature like
def myTernary (condition: Boolean, ifTrue: => Any, ifFalse => Any)
Although the correct functionality could be implemented, the signature requires condition
, ifTrue
, and ifFalse
to all be passed as arguments to some method, where what we really want is condition
, followed by a ?
, followed by ifTrue
, etc.
Instead, we can define a method called ?
on a class Ternable
, and provide an implicit conversion from Boolean
to Ternable
, like
object Implicits {
implicit class Ternable (condition: Boolean) {
def ? (ifTrue: => Any, ifFalse: => Any): Any = {
if (condition) ifTrue else ifFalse
}
}
}
This gets us a bit closer, as we can now write code like
import Implicits._
(3 > 2).?("fine", "uh...") // val res0: Any = fine
(3 < 2).?("what", "yeah") // val res1: Any = yeah
We can't drop the .
and write
(3 < 2) ? ("what", "yeah")
...though, because that syntactic sugar only works when the function (in this case ?
) takes a single argument. This one takes two.
We also want to add a :
symbol in between the ifTrue
and ifFalse
. Scala's associativity rules say that any operators ending in :
are right-associative, meaning that the argument on the right-hand side of the :
-- ifFalse
-- is the one for which the operator :
must be defined.
Since ifFalse
is of type Any
, we need another implicit conversion to add a :
method to the Any
type, but what should the method signature look like?
Because ?
has a higher precedence than :
, the first part of the expression will be evaluated first
var x = (condition ? ifTrue) : ifFalse
So :
can take a single argument... but what should that argument's type be? ifTrue
could evaluate to Any
kind of value, so how can we signal that (1) condition
was true
, ifTrue
was evaluated, and we should return that value vs. (2) condition
was false
, ifTrue
was not evaluated, and we need to evaluate ifFalse
?
One way is to change the method signature of ?
. We can have it return an Option[Any]
-- a Some
in case (1) and a None
in case (2)
object Implicits {
implicit class Ternable (condition: Boolean) {
def ? (ifTrue: => Any): Option[Any] = {
if (condition) Some(ifTrue) else None
}
}
}
Because we've now reduced the arity of ?
from 2 to 1, we can also make use of the syntactic sugar which lets us drop the .()
notation for method calls
import Implicits._
(3 > 2) ? "hey" // val res0: Option[Any] = Some(hey)
(3 < 2) ? "hey" // val res1: Option[Any] = None
This means that our :
method should accept an Option[Any]
as its argument type
object Implicits {
...
implicit class Colonable (ifFalse: => Any) {
def : (intermediate: Option[Any]): Any =
intermediate match {
case Some(ifTrue) => ifTrue
case None => ifFalse
}
}
}
This would work beautifully... if :
weren't a part of Scala's basic language syntax. Remember that :
is used to define the type of an object (as in val x: String
), so if we try to define the method as above, we get a compiler error ("identifier expected").
Since we want to define an implicit method on Any
(which has very few built-in methods), we can just pick another operator which sort of looks like :
-- how about |
? It already means "or" in many contexts, which is more or less what it means here. Remember, though, that we still need the :
as the last character in the method name to get the right associativity
object Implicits {
...
implicit class Colonable (ifFalse: => Any) {
def |: (intermediate: Option[Any]): Any =
intermediate match {
case Some(ifTrue) => ifTrue
case None => ifFalse
}
}
}
Check it out!
import Implicits._
(3 > 2) ? "true" |: "false" // val res0: Any = true
(3 < 2) ? "true" |: "false" // val res1: Any = false
It works! With syntax almost as clean as in Java. (Never thought I would say that with a straight face.)
How can we improve on this? Well, the return type is currently Any
, which is less than ideal. Can we infer a narrower type from the types of ifTrue
and ifFalse
?
We could use some type class craziness to try to find the narrowest common supertype (NCS) of ifTrue
and ifFalse
, but for any heterogenous pair of value types ("primitives"), the NCS is AnyVal
, which is not extremely helpful.
Instead, a more Scala-like solution might be to use an Either
type
object Implicits {
implicit class Ternable (condition: Boolean) {
def ? [T](ifTrue: => T): Option[T] = {
if (condition) Some(ifTrue) else None
}
}
implicit class Colonable [T, F](ifFalse: => F) {
def |: (intermediate: Option[T]): Either[T, F] =
intermediate match {
case Some(ifTrue) => Left(ifTrue)
case None => Right(ifFalse)
}
}
}
import Implicits._
((3 > 2) ? "true" |: 42) match {
case Left(v) => s"$v is a ${v.getClass}"
case Right(v) => s"$v is a ${v.getClass}"
}
// prints: true is a class java.lang.String
((3 < 2) ? "true" |: false) match {
case Left(v) => s"$v is a ${v.getClass}"
case Right(v) => s"$v is a ${v.getClass}"
}
// prints: false is a boolean
So there you have it! A pretty close approximation of the ternary operator in Scala, which maintains as much type information as possible, with minimal noise.
Let me know what you think in the comments!