Null safe operations in scala

(This is one of those posts where I'm pretty sure I'm reinventing something that others already found. But for the sake of those who haven't...)

 

How many times have you used StringUtils.isEmpty(s)? Isn't the syntax annoying? Plus, you need to know where to import it from. What about a null collection? A class of your own that can have null instances?

 

(For those that are now shouting 'you should use Option': don't worry, we'll get there)

 

Lets start with just String:

 

class SafeStringOps(s: String) {
  def safeIsEmpty = if (s == null) true else s.isEmpty
}

implicit def toSafeOps(s: String) = new SafeStringOps(s)

 

And then:

scala> "hi".safeIsEmpty
res1: Boolean = false

scala> (null:String).safeIsEmpty
res2: Boolean = true

 

But this is just a pattern to something more general. What we need is something to wrap instances in a presentation that is safe to use

 

trait Safe[A, B] {
  def safeInstance(a: A): B
}

class SafeWrapper[A](a: A) {
  def safe[B](implicit ops: Safe[A, B]) = ops.safeInstance(a)
}

implicit def toSafeOps[A](a: A) = new SafeWrapper(a)

class SafeStringOps(s: String) {
  def issEmpty = if (s == null) true else s.isEmpty
}

implicit object SafeString extends Safe[String, SafeStringOps] {
   def safeInstance(s: String) = new SafeStringOps(s)
}

 

scala> (null:String).safe.isEmpty
res3: Boolean = true

So when we call 'safe' on an instance, the compiler finds an implicit instance of Safe and the type of the result of safe is the second type parameter of Safe. So in the string example, safe returns a SafeStringOps. This is called functional dependencies.

 

The benefit with this schema is that the compiler will look for 'Safe' instances in companion objects of the related types. So if we have a class MyClass, we can put the instance of Safe in its companion object:

class MyClass {
  def isEmpty = false
}
  
object MyClass {
  class SafeMyClas(m: MyClass) {
    def isEmpty = if (m == null) true else m.isEmpty
  }
	
  implicit val safe = new Safe[MyClass, SafeMyClass] {
    def safeInstance(m: MyClass) = new SafeMyClass(m)
  }
}

 

Now creation of special classess and wrappers is a bit tedious. We can use the null object pattern

trait MyClassBase {
  def isEmpty: Boolean
}

class MyClass extends MyClassBase {
  def isEmpty = false
}

object MyClass {
  object NullMyClass extends MyClassBase {
    def isEmpty = true
  }
	
  implicit val safe = new Safe[MyClass, MyClassBase] {
    def safeInstance(m: MyClass) = if (m == null) NullMyClass else m
  }
}

Finally, as hinted at the beginning of the article, one should normally not return null from methods and instead the return type should be Option[X] for methods where there's a possibility of nothing to return. So this pattern is good for dealing with Java APIs. However, even with Options, we may need a behavior where we have a default implementation of a method for a case when None is returned. The safe mechanism can be used there also.

 

class SafeOption[T <: AnyRef, U](safe: Safe[T, U]) extends Safe[Option[T], U] {
  def safeInstance(o: Option[T]) = if (o == null || o.isEmpty) safe.safeInstance(null.asInstanceOf[T]) else safe.safeInstance(o.get)
}

implicit def toSafeOption[T <: AnyRef, U](implicit safe: Safe[T, U]) = new SafeOption[T, U](safe)

 

scala> (None:Option[String]).safe.isEmpty
res4: Boolean = true

 

Thank you for your interest!

We will contact you as soon as possible.

Send us a message

Oops, something went wrong
Please try again or contact us by email at info@tikalk.com