Implementing a Ternary Operator in Scala

Andrew (he/him) - Jun 26 '20 - - Dev Community

Photo by Magda Ehlers from Pexels


Scala doesn't have the traditional ternary operator from Java

// java
var x = condition ? ifTrue : ifFalse
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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)
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We can't drop the . and write

(3 < 2) ? ("what", "yeah")
Enter fullscreen mode Exit fullscreen mode

...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
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
      }
  }
}
Enter fullscreen mode Exit fullscreen mode

Check it out!

import Implicits._

(3 > 2) ? "true" |: "false" // val res0: Any = true
(3 < 2) ? "true" |: "false" // val res1: Any = false
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player