How to write a proper null-safe coalescing operator in scala?

Viewed 5253

Having seen the answers coming out of questions like this one involving horror shows like trying to catch the NPE and dredge the mangled name out of the stack trace, I am asking this question so I can answer it.

Comments or further improvements welcome.

2 Answers

Added orElse

case class ?:[T](x: T) {
  def apply(): T = x
  def apply[U >: Null](f: T => U): ?:[U] =
    if (x == null) ?:[U](null)
    else ?:[U](f(x))
  def orElse(y: T): T = 
    if (x == null) y
    else x
}
scala> val x = ?:(obj)(_.subField)(_.subSubField).orElse("not found")
x: java.lang.String = not found

Or if you prefer named syntax as opposed to operator syntax

case class CoalesceNull[T](x: T) {
  def apply(): T = x
  def apply[U >: Null](f: T => U): CoalesceNull[U] =
    if (x == null) CoalesceNull[U](null)
    else CoalesceNull[U](f(x))
  def orElse(y: T): T =
    if (x == null) y
    else x
}
scala> val x = CoalesceNull(obj)(_.subField)(_.subSubField).orElse("not found")
x: java.lang.String = not found

More examples

case class Obj[T](field: T)

  test("last null") {
    val obj: Obj[Obj[Obj[Obj[String]]]] = Obj(Obj(Obj(Obj(null))))
    val res0 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field)()
    res0 should === (null)
    val res1 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field).orElse("not found")
    res1 should === ("not found")
  }

  test("first null") {
    val obj: Obj[Obj[Obj[Obj[String]]]] = null
    val res0 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field)()
    res0 should === (null)
    val res1 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field).orElse("not found")
    res1 should === ("not found")
  }

  test("middle null") {
    val obj: Obj[Obj[Obj[Obj[String]]]] = Obj(Obj(null))
    val res0 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field)()
    res0 should === (null)
    val res1 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field).orElse("not found")
    res1 should === ("not found")
  }

  test("not null") {
    val obj: Obj[Obj[Obj[Obj[String]]]] = Obj(Obj(Obj(Obj("something"))))
    val res0 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field)()
    res0 should === ("something")
    val res1 = CoalesceNull(obj)(_.field)(_.field)(_.field)(_.field).orElse("not found")
    res1 should === ("something")
  }
}
Related