Giter Site home page Giter Site logo

ryanstull / scalanullsafe Goto Github PK

View Code? Open in Web Editor NEW
18.0 3.0 0.0 299 KB

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

License: MIT License

Scala 99.52% Java 0.48%
scala null-safety null null-check nullability efficiency macros macro

scalanullsafe's Introduction

ScalaNullSafe

The purpose of this macro is to provide a quick, easy, readable/writable, and efficient way to make code null-safe 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 ✔️ ✔️ ⚠️
Monocle Optional (lenses) ✔️ 💀
thoughtworks NullSafe DSL ✔️ ✔️ ⚠️

Key: ✔️ = Good, ⚠️ = Sub-optimal, ⛔ = Bad, 💀 = Horrible

How to use

Add the dependency:

Maven Central

libraryDependencies += "com.ryanstull" %% "scalanullsafe" % "1.2.6" % "provided"

* 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

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, and another that just checks if a property is defined.

opt(a.b.c.d.e.s) //Returns None
notNull(a.b.c.d.e.s) //Returns false

opt(a2.b.c.d.e.s) //Returns Some("Hello")
notNull(a2.b.c.d.e.s) //Returns true

How it works

? macro

The macro works by translating an expression, 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, translating ?(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

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

Safe translation

All of the above work for method invocation as well as property access, and the two can be intermixed. For example:

?(someObj.methodA().field1.twoArgMethod("test",1).otherField)

will be translated properly.

Also the macro 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.

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,"") == "")

?? 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. To be more explicit, the ?? macro would translate ??(a.b.c,a2.b.c)(default) into

{
    val v1 = if(a != null){
      val b = a.b
      if(b != null){
        val c = b.c
        if(c != null){
          c
        } else null
      } else null
    } else null
    if(v1 != null) v1
    else {
        val v2 = if(a2 != null){
          val b = a2.b
          if(b != null){
            val c = b.c
            if(c != null){
              c
            } else null
          } else null
        } else null
        if (v2 != null) v2
        else default
    }
}

Compared to the ? macro in the case of a single arg, the ?? macro check that that 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.

Efficient null-checks

The macro is also smart about what it checks for null, so anything that is <: 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             thrpt   20  230.157 ± 0.572  ops/us
[info] Benchmarks.ScalaNullSafeAbsent       thrpt   20  428.124 ± 1.625  ops/us
[info] Benchmarks.ScalaNullSafePresent      thrpt   20  232.066 ± 0.575  ops/us
[info] Benchmarks.explicitSafeAbsent        thrpt   20  429.090 ± 0.842  ops/us
[info] Benchmarks.explicitSafePresent       thrpt   20  231.400 ± 0.660  ops/us
[info] Benchmarks.optionSafeAbsent          thrpt   20  139.369 ± 0.272  ops/us
[info] Benchmarks.optionSafePresent         thrpt   20  129.394 ± 0.102  ops/us
[info] Benchmarks.loopSafeAbsent            thrpt   20  114.330 ± 0.113  ops/us
[info] Benchmarks.loopSafePresent           thrpt   20   59.513 ± 0.097  ops/us
[info] Benchmarks.nullSafeNavigatorAbsent   thrpt   20  274.222 ± 0.441  ops/us
[info] Benchmarks.nullSafeNavigatorPresent  thrpt   20  181.356 ± 1.538  ops/us
[info] Benchmarks.tryCatchSafeAbsent        thrpt   20  254.158 ± 0.686  ops/us
[info] Benchmarks.tryCatchSafePresent       thrpt   20  230.081 ± 0.659  ops/us
[info] Benchmarks.monocleOptionalAbsent     thrpt   20   77.755 ± 0.800  ops/us
[info] Benchmarks.monocleOptionalPresent    thrpt   20   36.446 ± 0.506  ops/us
[info] Benchmarks.nullSafeDslAbsent         thrpt   30  228.660 ± 0.475  ops/us
[info] Benchmarks.nullSafeDslPresent        thrpt   30  119.723 ± 0.506  ops/us
[success] Total time: 3909 s, completed Feb 24, 2019 3:03:02 PM

You can find the source code for the JMH benchmarks here. If you want to run the benchmarks yourself, just run sbt bench, or sbt quick-bench for a shorter run. These benchmarks compare all of the known ways (or at least the ways that I know of) to handle null-safety in scala.

The reason ScalaNullSafe performs the best is because there are no extraneous method calls, memory allocations, or exception handling, which all of the other solutions use. By leveraging the power of macros we are able to produce theoretically optimal bytecode, whose performance is equivalent to the explicit null safety approach.

Why?

Some people have questioned the reason for this library's existence since, in Scala, the idiomatic way to handle potentially absent values is to use Option[A]. The reason this library is needed is because there will be situations where you need to extract deeply nested data, in a null-safe way, that was not defined using Option[A]. This mostly happens when interoping with Java, but could also occur with any other JVM language. The original reason this library was created was to simplify a large amount of code that dealt with extracting values out of highly nested Avro data structures.

Notes

  • Using the ? macro on an expression whose type is <: AnyVal, will result in returning the corresponding java wrapper type. For example ?(a.getInt) will return java.lang.Integer instead of Int because the return type for this macro must be nullable. The conversions are the default ones defined in scala.Predef

  • If you're having trouble with resolving the correct method when using the ? macro with a default arg, try explicitly specifying the type of the default

scalanullsafe's People

Contributors

ryanstull avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

scalanullsafe's Issues

Make the macros work with function calls as well

If there is a function call in the expression passed to the macros: i.e:

?(SomeFunc(a.b).c)

Currently the macros will fail, but the macros could handle this case if it considers the function as part of the first transformation. I.e.

if (a!=null){
    val b_prime = SomeFunc(a.b)
    if(b_prime != null) {
       b_prime.c
    } else null
} else null

Add support for methods which wrap arguments in implicit classes

val a: String = null
?(a.toInt) // throws NumberFormatException

The above snippet fails due to Scala wrapping a in an implicit class and the macro not knowing how to check for null properly in this instance. The snippet below is what the above code compiles to. The macro should instead check if a is != null and apply the wrapper in the body of the if.

{
  <synthetic> val fresh$macro$1: scala.collection.StringOps = scala.Predef.augmentString(a);
  if (fresh$macro$1.$bang$eq(null))
    fresh$macro$1.toInt
  else
    null
}

Add benchmarks

Compare against:

Simple access (Unsafe)
Manual full safety
Option flatmap
For loop, option flatmap
Nullsafe navigator lambda
Simple catch NPE

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.