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!
Top comments (6)
I found the same inconvenience when working with Scala, the ternary operator just does not exist. Thanks for sharing your interesting implementations.
My opinion is, I tend to separate
Either
and ternary operator since it loses the conciseness when used together.My preference:
This is nicer, but it only works if
ifTrue
andifFalse
have the same type. You could do something likeThis notation has the benefit that
|
is often used for type unions (inmatch
statements), which is like an OR for types, while&
could be used for type intersections, a type AND.I don't oppose the idea of using sum type in ternary operator, you just delegate it to the next procedure to do the pattern matching and there will be no benefit than just using
if then else
.Just for comparison, compare this
with this
The conciseness is obvious.
Fair enough! So what you're saying is you'd have the
ifTrue
andifFalse
themselves returnEither
s, if necessary, likeIs that right?
I wonder if there's an implicit conversion that would let us do away with the
Left()
andRight()
...That is a good example to ensure type consistency with minimal effort, you don't need
|:
and&:
at the same time and achieve the same result (Option[Either[T, F]]
).One of my main point here is do not over obsess with data type generalization and construct unnecessary structures.