A Variation on Scala’s Either Type

I’ve been using Scala’s Either type a bit recently, and found a couple of minor annoyances with it. So as a learning exercise I tried to make a variation of Either, and this is what I came up with. I also wanted to experiment with syntax a little. See what you think.

Either is a disjunctive type with two possible values: a left and a right. It places no meaning at all on left or right, but 99% of its uses in practice involve putting normal, expected values in right, and errors or other “exceptional” values in left. This means that almost all the time you end up getting the RightProjection, e.g.:

for {
    v1 <- foo(stuff).right
    v2 <- bar(stuff).right
    v3 <- baz(stuff).right
}
yield something

Whereas I’ve never used .left even once.

The second minor issue is that Either has some limitations with patterns in for expressions, because of how it is forced to implement the filter method. See this Stack Overflow question for the details.

So I created a type that is explicitly designed to handle the possibility of “exception” conditions. Because the word “exception” already has a well known meaning on the JVM, I called this type Anomaly instead. Actually, I ended up calling the type |: because I thought it worked nicely syntactically. Here’s a comparison with Either:

val e1: Either[String, Int] = Right(30)
val e2: Either[String, (Int, Int)] = Right((10, 2))
val e3 = for {
    i <- e1.right
    j <- e2.right
}
yield i + j._1 + j._2
val e4: Either[String, Int] = Left("error")

vs “Anomaly”:

val a1: String |: Int = Expected(30)
val a2: String |: (Int, Int) = Expected((10, 2))
val a3 = for {
    i <- a1
    (j, k) <- a2
}
yield i + j + k
val a4: String |: Int = Anomaly("error")

The |: operator is right-associative, which helps for the occasional cases where you have an Anomaly within an Anomaly, eg. String |: String |: Int is equivalent to Either[String, Either[String, Int]]. Of course, depending on your taste you could reverse the order of type arguments, using an operator like :|, or use a name instead of an operator.

There’s a cost to making the for expression pattern matching work like this, however. In addition to the Anomaly and Expected cases, I had to introduce a NoValue case object, which is what is returned when a filter does not match. This is annoying, because now folding or pattern matching must account for three cases instead of two. And unless you have explicit filters or patterns matching particular values, the NoValue outcome won’t come up, so it’s a pain to have to check for it. What I ended up doing was providing an alternate two-parameter fold method that calls error in case of a NoValue. You just have to be careful about only calling it when you know it’s safe.

All-in-all I’m still wondering whether this experiment is worthwhile, but here’s the code in case anyone is interested. There are other utility methods that could be added:

sealed abstract class |:[+A, +E] {
  def anomaly: A
  def expected: E
  def isExpected: Boolean = false
  def isAnomaly: Boolean = false
  def isNoValue: Boolean = false

  def map[X](f: E => X): A |: X

  def flatMap[X, AA >: A](f: E => AA |: X): AA |: X

  def withFilter(f: E => Boolean): A |: E = this
  final def filter(f: E => Boolean): A |: E = withFilter(f)

  def foreach(f: E => Unit): Unit = {}

  final def fold[X](fExpected: E => X, fAnomaly: A => X, fNoValue: => X): X = this match {
    case Anomaly(a) => fAnomaly(a)
    case Expected(e) => fExpected(e)
    case NoValue => fNoValue
  }

  final def fold[X](fExpected: E => X, fAnomaly: A => X): X = this match {
    case Anomaly(a) => fAnomaly(a)
    case Expected(e) => fExpected(e)
    case NoValue => error("Attempt to fold NoValue case")
  }

  final def join[X, AA >: A](implicit ev: E <:< (AA |: X)): AA |: X = this match {
    case Anomaly(a) => Anomaly(a)
    case Expected(e) => e
    case NoValue => NoValue
  }
}

final case class Expected[+A, +E](expected: E) extends (A |: E) {
  override def isExpected = true
  def anomaly: A = Predef.error("Not an anomaly")

  override def withFilter(f: E => Boolean) =
    if (f(expected)) this else NoValue

  def flatMap[X, AA >: A](f: E => AA |: X) = f(expected)

  def map[X](f: E => X) = Expected[A, X](f(expected))

  override def foreach(f: E => Unit) {
    f(expected)
  }
}

object Expected {
  def cond[A, E](test: Boolean, expected: => E, anomaly: => A): A |: E =
    if (test) Expected(expected) else Anomaly(anomaly)
}

final case class Anomaly[+A, +E](anomaly: A) extends (A |: E) {
  override def isAnomaly = true
  def expected: E = Predef.error("Not an expected value")

  def flatMap[X, AA >: A](f: E => AA |: X) = Anomaly[AA, X](anomaly)

  def map[X](f: E => X) = Anomaly[A, X](anomaly)
}

object Anomaly {
  def cond[A, E](test: Boolean, anomaly: => A, expected: => E): A |: E =
    if (test) Anomaly(anomaly) else Expected(expected)
}

case object NoValue extends (Nothing |: Nothing) {
  override def isNoValue = true
  def anomaly = Predef.error("Not an anomaly")
  def expected = Predef.error("No value")

  def flatMap[X, AA >: Nothing](f: Nothing => AA |: X) = this

  def map[X](f: Nothing => X) = this
}

Comments !

blogroll

social