ScalaNullSafe
A macro-based library for writing efficient and readable null-safe code in Scala.
Install / Use
/learn @ryanstull/ScalaNullSafeREADME
ScalaNullSafe
The purpose of this library is to provide a quick, easy, readable/writable, and efficient way to do null-safe traversals in Scala.
Quick comparison of null-safe implementations:
| Implementation | Null-safe | Readable & Writable | Efficient | |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------- |------------------- |----------- | | 🎉 ScalaNullSafe 🎉 | ✔️ | ✔️ | ✔️ | | Normal access | ⛔ | ✔️ | ✔️ | | Explicit null-checks | ✔️ | ⛔ | ✔️ | | Option flatMap | ✔️ | ⚠️ | ⛔ | | For loop flatMap | ✔️ | ⚠️ | ⛔ | | Null-safe navigator | ✔️ | ⚠️ | ⚠️ | | Try-catch NPE | ✔️ | ✔️ | ⚠️ | | thoughtworks NullSafe DSL | ✔️ | ✔️ | ⚠️ | | Monocle Optional (lenses) | ✔️ | 💀 | 💀 |
Key: ✔️ = Good, ⚠️ = Sub-optimal, ⛔ = Bad, 💀 = Horrible
How to use
Add the dependency:
libraryDependencies += "com.ryanstull" %% "scalanullsafe" % "1.4.0" % "provided"
<sub>* Since macros are only used at compile time, if your build tool has a way to specify compile-time-only dependencies, you can use that for this library</sub>
Example use:
import com.ryanstull.nullsafe._
case class A(b: B)
case class B(c: C)
case class C(d: D)
case class D(e: E)
case class E(s: String)
val a = A(B(C(null)))
?(a.b.c.d.e.s) //No NPE! Just returns null
val a2 = A(B(C(D(E("Hello")))))
?(a2.b.c.d.e.s) //Returns "Hello"
There's also a variant that returns an Option[A] when provided an expression of type A,
another that just checks if a property is defined, and it's inverse.
opt(a.b.c.d.e.s) //Returns None
notNull(a.b.c.d.e.s) //Returns false
isNull(a.b.c.d.e.s) //Returns true
opt(a2.b.c.d.e.s) //Returns Some("Hello")
notNull(a2.b.c.d.e.s) //Returns true
isNull(a2.b.c.d.e.s) //Returns false
How it works
? macro
The macro works by transforming an expression at compile-time, inserting null-checks before each intermediate result is used; turning
?(a.b.c), for example, into:
if(a != null){
val b = a.b
if(b != null){
b.c
} else null
} else null
Or for a longer example, transforming ?(a.b.c.d.e.s) into:
if(a != null){
val b = a.b
if(b != null){
val c = b.c
if(c != null){
val d = c.d
if(d != null){
val e = d.e
if(e != null){
e.s
} else null
} else null
} else null
} else null
} else null
Custom default for ?
For the ? macro, you can also provide a custom default instead of null, by passing it in as the second
parameter. For example:
case class Person(name: String)
val person: Person = null
assert(?(person.name,"Jeff") == "Jeff")
opt macro
The opt macro is very similar, translating opt(a.b.c) into:
if(a != null){
val b = a.b
if(b != null){
Option(b.c)
} else None
} else None
notNull macro
And the notNull macro, translating notNull(a.b.c) into:
if(a != null){
val b = a.b
if(b != null){
b.c != null
} else false
} else false
isNull macro
And the isNull macro, translating isNull(a.b.c) into:
if(a != null){
val b = a.b
if(b != null){
b.c == null
} else true
} else true
?? macro
There's also a ?? (null coalesce operator) which is used to select the first non-null value from a var-args list of expressions.
case class Person(name: String)
val person = Person(null)
assert(??(person.name)("Bob") == "Bob")
val person2: Person = null
val person3 = Person("Sally")
assert(??(person.name,person2.name,person3.name)("No name") == "Sally")
The null-safe coalesce operator also rewrites each arg so that it's null safe. So you can pass in a.b.c as an expression
without worrying if a or b are null.
A simple but accurate way to think about how the ?? macro transforms its arguments would be like this:
{
val v1 = ?(arg1)
if(v1 != null) v1
else {
<next> or <default>
}
}
So in the example above we would have:
{
val v1 = ?(person.name)
if (v1 != null) v1
else {
val v2 = ?(person2.name)
if (v2 != null) v2
else {
val v3 = ?(person3.name)
if (v3 != null) v3
else default
}
}
}
To be fully explicit, the ?? macro would transform the above example to:
{
val v1 = if(person!=null){
person.name
} else null
if(v1 != null) v1
else {
val v2 = if(person2!=null) {
person2.name
} else null
if (v2 != null) v2
else {
val v3 = if(person3!=null){
person3.name
} else null
if (v3 != null) v3
else "No name"
}
}
}
?? compared to ?
Compared to the ? macro, in the case of a single arg, the ?? macro checks that the entire expression is not null; whereas
the ? macro would just check that the preceding elements (e.g. a and b in a.b.c) aren't null before returning the default value.
For example consider the following example:
case class A(b: B)
case class B(c: C)
case class C(s: String)
val a = A(B(C(null)))
assert(?(a.b.c.s, "Default") == null)
assert(??(a.b.c.s)("Default") == "Default")
For ?, the default value only gets used if there would've been a NullPointerException. So the return value of ? could still be null even if you supply a default.
Safe translation
All of the above work for method invocation as well as property access, and the two can be freely intermixed. For example:
?(someObj.methodA().field1.twoArgMethod("test",1).otherField)
will be translated properly.
Also the macros will make the arguments to method and function calls null-safe as well:
?(a.b.c.method(d.e.f))
So you don't have to worry if d or e would be null.
Efficient null-checks
The macros are also smart about what they check for null; so any intermediate results that are <: AnyVal will not be checked for null. For example:
case class A(b: B)
case class B(c: C)
case class C(s: String)
?(a.b.c.s.asInstanceOf[String].charAt(2).*(2).toString.getBytes.hashCode())
Would be translated to:
if (a != null)
{
val b = a.b;
if (b != null)
{
val c = b.c;
if (c != null)
{
val s = c.s;
if (s != null)
{
val s2 = s.asInstanceOf[String].charAt(2).$times(2).toString();
if (s2 != null)
{
val bytes = s2.getBytes();
if (bytes != null)
bytes.hashCode()
else
null
}
else
null
}
else
null
}
else
null
}
else
null
}
else
null
Performance
Here's the result of running the included jmh benchmarks:

[info] Benchmark Mode Cnt Score Error Units
[info] Benchmarks.fastButUnsafe thr
