DEV Community

Cover image for Implementing a Ternary Operator in Scala
Andrew (he/him)
Andrew (he/him)

Posted on

Implementing a Ternary Operator in Scala

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!

Top comments (6)

Collapse
 
dividedbynil profile image
Kane Ong • Edited

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:

object Implicits {
  implicit class Ternable (condition: Boolean) {
    def ? [T](ifTrue: => T): Option[T] = {
      if (condition) Some(ifTrue) else None
    }
  }
  implicit class Colonable [T](ifFalse: => T) {
    def |: (intermediate: Option[T]): T =
      intermediate match {
        case Some(ifTrue) => ifTrue
        case None => ifFalse
      }
  }
}

import Implicits._

object Main {
  def main(args: Array[String]): Unit = {
    val a = (3 > 2) ? "true" |: "false"
    val b = (3 < 2) ? "true" |: "false"

    val c = (1 > 0) ? 1 |: 0
    val d = (1 < 0) ? 1 |: 0

    println(s"a: $a\nb: $b\nc: $c\nd: $d")
  }
}

Collapse
 
awwsmm profile image
Andrew (he/him) • Edited

This is nicer, but it only works if ifTrue and ifFalse have the same type. You could do something like

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)
      }
    def &:(intermediate: Option[F]): F =
      intermediate match {
        case Some(ifTrue) => ifTrue
        case None => ifFalse
      }
  }
}

import Implicits._

val diff1 = (3 > 2) ? 42 |: 42L    // Either[Int,Long] = Left(42)
val diff2 = (3 < 2) ? 42 |: 42L    // Either[Int,Long] = Right(42)

val same1 = (3 > 2) ? "gt" &: "lt" // String = gt
val same2 = (3 < 2) ? "lt" &: "gt" // String = gt

This notation has the benefit that | is often used for type unions (in match statements), which is like an OR for types, while & could be used for type intersections, a type AND.

Collapse
 
dividedbynil profile image
Kane Ong • Edited

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.

Thread Thread
 
dividedbynil profile image
Kane Ong • Edited

Just for comparison, compare this

object Main {
  def main(args: Array[String]): Unit = {

    val n = 0

    if (n == 0) 
      println(s"$n == 0 is true")
    else 
      println(s"${3/n}") 

  }
}

with this

import Implicits._

object Main {
  def main(args: Array[String]): Unit = {

    val n = 0

    ((n == 0) ? true |: n) match {
      case Left(v) => println(s"$n == 0 is $v")
      case Right(v) => println(s"${3/v}") 
    } 

  }
}

The conciseness is obvious.

Thread Thread
 
awwsmm profile image
Andrew (he/him) • Edited

Fair enough! So what you're saying is you'd have the ifTrue and ifFalse themselves return Eithers, if necessary, like

(3 < 2) ? Left(42) |: Right("forty-two")

Is that right?

I wonder if there's an implicit conversion that would let us do away with the Left() and Right()...

Thread Thread
 
dividedbynil profile image
Kane Ong • Edited

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.