SkillAgentSearch skills...

ScalaNullSafe

A macro-based library for writing efficient and readable null-safe code in Scala.

Install / Use

/learn @ryanstull/ScalaNullSafe
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

ScalaNullSafe

The purpose of this library is to provide a quick, easy, readable/writable, and efficient way to do null-safe traversals in Scala.

Scala CI

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:

Maven Central Scala version support

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:

Throughput

[info] Benchmark                             Mode  Cnt    Score   Error   Units
[info] Benchmarks.fastButUnsafe             thr
View on GitHub
GitHub Stars44
CategoryContent
Updated21d ago
Forks0

Languages

Scala

Security Score

95/100

Audited on Mar 10, 2026

No findings