4

In Scala, if I have

hub = myBicycle.getFrontWheel.getHub()

and it is possible the front wheel is missing, i.e. myBicycle.getFrontWheel() == null, and I just want hub to be assigned null in such a case, what is the most concise way to express that?

I'm currently having to do

hub = if (myBicycle.getFrontWheel() == null) null else myBicycle.getFrontWheel.getHub()

and it gets worse when the chain of accessors is even longer.

Not being familiar with Scala macros, I'm wondering if it's possible to write a Scala macro that somehow captures the method name and applies it only if the object reference is non-null?

Jacek Laskowski
  • 72,696
  • 27
  • 242
  • 420
Michael Malak
  • 628
  • 8
  • 19

3 Answers3

5

Actually, I have recently written exactly such a macro, inspired by a question about null-safe dereferences in Scala. By the way, this is very similar question to this one and contains a long discussion about what you can do to achieve this, including usage of Option, fancy ways of catching NPEs and such.

A few remarks about macro that I have written (source included below):

  • It returns an Option, and it will be None when at some point there was a null dereference.
  • It translates every member access by dot into a null-guarded access using if-else that returns None when the prefix is null.
  • It got a little more complicated that I imagined it to be...
  • There are some corner cases for which it will not work, the one I know about is when using getClass method - which is handled specially by the compiler in regards of its return type.
  • There is a potential inconsistency regarding implicit conversions. Imagine that you have an expression a.b and b is reached through an implicit conversion, so effectively, this expression is something like conv(a).b. Now, the question arises: Should we check if a is null or conv(a) is null or both? Currently, my macro checks only if a is null as it seemed a little more natural to me, but this may not be desired behaviour in some cases. Also, detection of an implicit conversion in my macro is a little hack, described in this question.

The macro:

def withNullGuards[T](expr: T): Option[T] = macro withNullGuards_impl[T]

def withNullGuards_impl[T](c: Context)(expr: c.Expr[T]): c.Expr[Option[T]] = {
  import c.universe._

  def eqOp = newTermName("==").encodedName
  def nullTree = c.literalNull.tree
  def noneTree = reify(None).tree
  def someApplyTree = Select(reify(Some).tree, newTermName("apply"))

  def wrapInSome(tree: Tree) = Apply(someApplyTree, List(tree))

  def canBeNull(tree: Tree) = {
    val sym = tree.symbol
    val tpe = tree.tpe

    sym != null &&
      !sym.isModule && !sym.isModuleClass &&
      !sym.isPackage && !sym.isPackageClass &&
      !(tpe <:< typeOf[AnyVal])
  }

  def isInferredImplicitConversion(apply: Tree, fun: Tree, arg: Tree) =
    fun.symbol.isImplicit && (!apply.pos.isDefined || apply.pos == arg.pos)

  def nullGuarded(originalPrefix: Tree, prefixTree: Tree, whenNonNull: Tree => Tree): Tree =
    if (canBeNull(originalPrefix)) {
      val prefixVal = c.fresh()
      Block(
        ValDef(Modifiers(), prefixVal, TypeTree(null), prefixTree),
        If(
          Apply(Select(Ident(prefixVal), eqOp), List(nullTree)),
          noneTree,
          whenNonNull(Ident(prefixVal))
        )
      )
    } else whenNonNull(prefixTree)

  def addNullGuards(tree: Tree, whenNonNull: Tree => Tree): Tree = tree match {
    case Select(qualifier, name) =>
      addNullGuards(qualifier, guardedQualifier =>
        nullGuarded(qualifier, guardedQualifier, prefix => whenNonNull(Select(prefix, name))))
    case Apply(fun, List(arg)) if (isInferredImplicitConversion(tree, fun, arg)) =>
      addNullGuards(arg, guardedArg =>
        nullGuarded(arg, guardedArg, prefix => whenNonNull(Apply(fun, List(prefix)))))
    case Apply(Select(qualifier, name), args) =>
      addNullGuards(qualifier, guardedQualifier =>
        nullGuarded(qualifier, guardedQualifier, prefix => whenNonNull(Apply(Select(prefix, name), args))))
    case Apply(fun, args) =>
      addNullGuards(fun, guardedFun => whenNonNull(Apply(guardedFun, args)))
    case _ => whenNonNull(tree)
  }

  c.Expr[Option[T]](addNullGuards(expr.tree, tree => wrapInSome(tree)))
}

EDIT

Here's an additional piece of code that makes the syntax nicer:

def any2question_impl[T, R >: T](c: Context {type PrefixType = any2question[T]})(default: c.Expr[R]): c.Expr[R] = {
  import c.universe._

  val Apply(_, List(prefix)) = c.prefix.tree
  val nullGuardedPrefix = withNullGuards_impl(c)(c.Expr[T](prefix))
  reify {
    nullGuardedPrefix.splice.getOrElse(default.splice)
  }
}

implicit class any2question[T](any: T) {
  def ?[R >: T](default: R): R = macro any2question_impl[T, R]
}

Finally, you can have code like this:

val str1: String = "hovercraftfullofeels"
val result1 = str1.substring(3).toUpperCase ? "THERE WAS NULL"
println(result1) // prints "ERCRAFTFULLOFEELS"

val str2: String = null
val result2 = str2.substring(3).toUpperCase ? "THERE WAS NULL"
println(result2) // prints "THERE WAS NULL"
Community
  • 1
  • 1
ghik
  • 10,706
  • 1
  • 37
  • 50
  • You don't have a null-safe "if" lying around? So when it evaluates to null it's like false? Then one could just write if (some.stuff.is sane()) and don't worry if some.stuff evaluates to null or not, it would just be short-cut'ed to "false" – andreak Aug 30 '13 at 19:00
  • @andreak I'm sorry, I'm not sure what you're asking. The macro works roughly by translating every `prefix.member` into `{val p = prefix; if(p != null) Some(p.member) else None}`. – ghik Aug 30 '13 at 20:29
4

Unless you're having to interoperate with fixed Java code, you should use Option instead of null; thus getFrontWheel() would return Option[Wheel], then you can use map/flatMap to go down the chain:

val hub:Option[Hub] = myBicycle.getFrontWheel().flatMap(wheel => wheel.getHub())
val spoke:Option[Spoke] = myBicycle.getFrontWheel().flatMap(wheel => wheel.getHub().map(hub => hub.getSpoke()))

(assumes hub.getSpoke() returns Spoke, not Option[Spoke])

The last example can be rewritten as

val spoke:Option[Spoke] =
  for (wheel <- myBicycle.getFrontWheel();
       hub <- wheel.getHub())
  yield hub.getSpoke()

If you really must deal with null you can easily convert the result by wrapping in Option:

val wheel:Option[Wheel] = Option(myBicycle.getFrontWheel())
Kristian Domagala
  • 3,686
  • 20
  • 22
0

You can write something like this:

def nil[B >: Null] (fun : => B) : B = {
  try fun
  catch {case e : NullPointerException => null}
}

Then use it like this:

val hub = nil(myBicycle.getFrontWheel.getHub)

It will affect null exceptions from nested calls too though. And if you want to write it the way you previously mentioned, do it at least like this:

val hub = Option(myBicycle.getFrontWheel()).map(_.getHub).getOrElse(null)
David Apltauer
  • 963
  • 2
  • 11
  • 23