{"id":15289170,"url":"https://github.com/ryanstull/scalanullsafe","last_synced_at":"2025-04-28T15:53:34.062Z","repository":{"id":88397626,"uuid":"159761768","full_name":"ryanstull/ScalaNullSafe","owner":"ryanstull","description":"A macro-based library for writing efficient and readable null-safe code in Scala.","archived":false,"fork":false,"pushed_at":"2025-03-16T18:29:14.000Z","size":335,"stargazers_count":43,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-28T15:53:28.120Z","etag":null,"topics":["efficiency","macro","macros","null","null-check","null-safety","nullability","scala"],"latest_commit_sha":null,"homepage":"","language":"Scala","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryanstull.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-11-30T03:13:33.000Z","updated_at":"2025-03-21T07:20:16.000Z","dependencies_parsed_at":"2024-10-14T20:10:45.634Z","dependency_job_id":null,"html_url":"https://github.com/ryanstull/ScalaNullSafe","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanstull%2FScalaNullSafe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanstull%2FScalaNullSafe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanstull%2FScalaNullSafe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanstull%2FScalaNullSafe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryanstull","download_url":"https://codeload.github.com/ryanstull/ScalaNullSafe/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251342718,"owners_count":21574243,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["efficiency","macro","macros","null","null-check","null-safety","nullability","scala"],"created_at":"2024-09-30T15:59:29.147Z","updated_at":"2025-04-28T15:53:34.030Z","avatar_url":"https://github.com/ryanstull.png","language":"Scala","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ScalaNullSafe\n\nThe purpose of this library is to provide a quick, easy, readable/writable, and efficient way to do null-safe traversals in Scala.\n\n[![Scala CI](https://github.com/ryanstull/ScalaNullSafe/actions/workflows/test.yml/badge.svg)](https://github.com/ryanstull/ScalaNullSafe/actions/workflows/test.yml)\n\n### Quick comparison of null-safe implementations:\n\n| Implementation      \t                                                                                                                                                                             | Null-safe \t| Readable \u0026 Writable | Efficient \t|\n|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------\t|-------------------\t|-----------\t|\n| 🎉 [**ScalaNullSafe**](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L132) 🎉        | ✔️         \t| ✔️                 \t| ✔️         \t|\n| [Normal access](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L27)        \t          | ⛔         \t| ✔️                 \t| ✔️         \t|\n| [Explicit null-checks](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L30-L45) \t      | ✔️         \t| ⛔                 \t| ✔️         \t|\n| [Option flatMap](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L74-L79)       \t      | ✔️         \t| ⚠️                 \t| ⛔         \t|\n| [For loop flatMap](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L82-L90)     \t      | ✔️         \t| ⚠️                 \t| ⛔         \t|\n| [Null-safe navigator](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L120-L123)  \t    | ✔️         \t| ⚠️                 \t| ⚠️         \t|\n| [Try-catch NPE](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L104-L109)        \t    | ✔️         \t| ✔️                 \t| ⚠️         \t|\n| [thoughtworks NullSafe DSL](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L169-L172) | ✔️         \t| ✔️\t                  | ⚠️         \t|\n| [Monocle Optional (lenses)](https://github.com/ryanstull/ScalaNullSafe/blob/e596852bd54fd3848bc9fa91bdee33f4024afde1/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L139-L162) | ✔️         \t| 💀\t                  | 💀         \t|\n\nKey: ✔️ = Good, ⚠️ = Sub-optimal, ⛔ = Bad, 💀 = Horrible\n\n## How to use\n\nAdd the dependency:\n\n[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.ryanstull/scalanullsafe_3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.ryanstull/scalanullsafe_3)\n[![Scala version support](https://index.scala-lang.org/ryanstull/scalanullsafe/scalanullsafe/latest-by-scala-version.svg?platform=jvm)](https://index.scala-lang.org/ryanstull/scalanullsafe/artifacts/scalanullsafe)\n\n```sbt\nlibraryDependencies += \"com.ryanstull\" %% \"scalanullsafe\" % \"1.4.0\" % \"provided\"\n```\n\u003csub\u003e* 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\u003c/sub\u003e\n\n### Example use:\n\n```scala\nimport com.ryanstull.nullsafe._\n\ncase class A(b: B)\ncase class B(c: C)\ncase class C(d: D)\ncase class D(e: E)\ncase class E(s: String)\n\nval a = A(B(C(null)))\n?(a.b.c.d.e.s) //No NPE! Just returns null\n\nval a2 = A(B(C(D(E(\"Hello\")))))\n?(a2.b.c.d.e.s) //Returns \"Hello\"\n```\n\nThere's also a variant that returns an `Option[A]` when provided an expression of type `A`, \nanother that just checks if a property is defined, and it's inverse.\n\n```scala\nopt(a.b.c.d.e.s) //Returns None\nnotNull(a.b.c.d.e.s) //Returns false\nisNull(a.b.c.d.e.s) //Returns true\n\nopt(a2.b.c.d.e.s) //Returns Some(\"Hello\")\nnotNull(a2.b.c.d.e.s) //Returns true\nisNull(a2.b.c.d.e.s) //Returns false\n```\n\n## How it works\n\n### `?` macro\n\nThe macro works by transforming an expression at compile-time, inserting null-checks before each intermediate result is used; turning\n`?(a.b.c)`, for example, into:\n\n```scala\nif(a != null){\n  val b = a.b\n  if(b != null){\n    b.c\n  } else null\n} else null\n```\n\nOr for a longer example, transforming `?(a.b.c.d.e.s)` into:\n\n```scala\nif(a != null){\n  val b = a.b\n  if(b != null){\n    val c = b.c\n    if(c != null){\n      val d = c.d\n      if(d != null){\n        val e = d.e\n        if(e != null){\n          e.s\n        } else null\n      } else null\n    } else null\n  } else null\n} else null\n```\n\n#### Custom default for `?`\n\nFor the `?` macro, you can also provide a custom default instead of `null`, by passing it in as the second\nparameter.  For example:\n\n```scala\ncase class Person(name: String)\n\nval person: Person = null\n\nassert(?(person.name,\"Jeff\") == \"Jeff\")\n```\n\n### `opt` macro\n\nThe `opt` macro is very similar, translating `opt(a.b.c)` into:\n\n```scala\nif(a != null){\n  val b = a.b\n  if(b != null){\n    Option(b.c)\n  } else None\n} else None\n```\n\n### `notNull` macro\n\nAnd the `notNull` macro, translating `notNull(a.b.c)` into:\n\n```scala\nif(a != null){\n  val b = a.b\n  if(b != null){\n    b.c != null\n  } else false\n} else false\n```\n\n### `isNull` macro\n\nAnd the `isNull` macro, translating `isNull(a.b.c)` into:\n\n```scala\nif(a != null){\n  val b = a.b\n  if(b != null){\n    b.c == null\n  } else true\n} else true\n```\n\n### `??` macro\n\nThere's also a `??` ([null coalesce operator](https://en.wikipedia.org/wiki/Null_coalescing_operator)) which is used to select the first non-null value from a var-args list of expressions.\n\n```scala\ncase class Person(name: String)\n\nval person = Person(null)\n\nassert(??(person.name)(\"Bob\") == \"Bob\")\n\nval person2: Person = null\nval person3 = Person(\"Sally\")\n\nassert(??(person.name,person2.name,person3.name)(\"No name\") == \"Sally\")\n```\n\nThe 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\nwithout worrying if `a` or `b` are `null`. \n\nA simple but accurate way to think about how the `??` macro transforms its arguments would be like this:\n\n```scala\n{\n    val v1 = ?(arg1)\n    if(v1 != null) v1\n    else {\n        \u003cnext\u003e or \u003cdefault\u003e\n    }\n}\n```\n\nSo in the example above we would have:\n\n```scala\n{\n    val v1 = ?(person.name)\n    if (v1 != null) v1\n    else {\n        val v2 = ?(person2.name)\n        if (v2 != null) v2\n        else {\n            val v3 = ?(person3.name)\n            if (v3 != null) v3\n            else default\n        }\n    }\n}\n```\n\nTo be fully explicit, the `??` macro would transform the above example to:\n\n```scala\n{\n    val v1 = if(person!=null){\n        person.name\n    } else null\n    if(v1 != null) v1\n    else {\n        val v2 = if(person2!=null) {\n            person2.name\n        } else null\n        if (v2 != null) v2\n        else {\n            val v3 = if(person3!=null){\n                person3.name\n            } else null\n            if (v3 != null) v3\n            else \"No name\"\n        }\n    }\n}\n```\n\n### `??` compared to `?`\n\nCompared to the `?` macro, in the case of a single arg, the `??` macro checks that the _entire_ expression is not null; whereas\nthe `?` 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.\n\nFor example consider the following example:\n\n```scala\ncase class A(b: B)\ncase class B(c: C)\ncase class C(s: String)\n\nval a = A(B(C(null)))\n\nassert(?(a.b.c.s, \"Default\") == null)\nassert(??(a.b.c.s)(\"Default\") == \"Default\")\n```\n\nFor `?`, 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.\n\n### Safe translation\n\nAll of the above work for method invocation as well as property access, and the two can be freely intermixed. For example:\n\n`?(someObj.methodA().field1.twoArgMethod(\"test\",1).otherField)`\n\nwill be translated properly.\n\nAlso the macros will make the arguments to method and function calls null-safe as well:\n\n`?(a.b.c.method(d.e.f))`\n\nSo you don't have to worry if `d` or `e` would be null.\n\n### Efficient null-checks\n\nThe macros are also smart about what they check for null; so any intermediate results that are `\u003c: AnyVal` will not be checked for null.  For example:\n\n```scala\ncase class A(b: B)\ncase class B(c: C)\ncase class C(s: String)\n\n?(a.b.c.s.asInstanceOf[String].charAt(2).*(2).toString.getBytes.hashCode())\n```\n\nWould be translated to:\n\n```scala\nif (a != null)\n  {\n    val b = a.b;\n    if (b != null)\n      {\n        val c = b.c;\n        if (c != null)\n          {\n            val s = c.s;\n            if (s != null)\n              {\n                val s2 = s.asInstanceOf[String].charAt(2).$times(2).toString();\n                if (s2 != null)\n                  {\n                    val bytes = s2.getBytes();\n                    if (bytes != null)\n                      bytes.hashCode()\n                    else\n                      null\n                  }\n                else\n                  null\n              }\n            else\n              null\n          }\n        else\n          null\n      }\n    else\n      null\n  }\nelse\n  null\n```\n\n## Performance\n\nHere's the result of running the included jmh benchmarks:\n\n![Throughput](throughput.png)\n\n```\n[info] Benchmark                             Mode  Cnt    Score   Error   Units\n[info] Benchmarks.fastButUnsafe             thrpt   20  230.157 ± 0.572  ops/us\n[info] Benchmarks.ScalaNullSafeAbsent       thrpt   20  428.124 ± 1.625  ops/us\n[info] Benchmarks.ScalaNullSafePresent      thrpt   20  232.066 ± 0.575  ops/us\n[info] Benchmarks.explicitSafeAbsent        thrpt   20  429.090 ± 0.842  ops/us\n[info] Benchmarks.explicitSafePresent       thrpt   20  231.400 ± 0.660  ops/us\n[info] Benchmarks.optionSafeAbsent          thrpt   20  139.369 ± 0.272  ops/us\n[info] Benchmarks.optionSafePresent         thrpt   20  129.394 ± 0.102  ops/us\n[info] Benchmarks.loopSafeAbsent            thrpt   20  114.330 ± 0.113  ops/us\n[info] Benchmarks.loopSafePresent           thrpt   20   59.513 ± 0.097  ops/us\n[info] Benchmarks.nullSafeNavigatorAbsent   thrpt   20  274.222 ± 0.441  ops/us\n[info] Benchmarks.nullSafeNavigatorPresent  thrpt   20  181.356 ± 1.538  ops/us\n[info] Benchmarks.tryCatchSafeAbsent        thrpt   20  254.158 ± 0.686  ops/us\n[info] Benchmarks.tryCatchSafePresent       thrpt   20  230.081 ± 0.659  ops/us\n[info] Benchmarks.monocleOptionalAbsent     thrpt   20   77.755 ± 0.800  ops/us\n[info] Benchmarks.monocleOptionalPresent    thrpt   20   36.446 ± 0.506  ops/us\n[info] Benchmarks.nullSafeDslAbsent         thrpt   30  228.660 ± 0.475  ops/us\n[info] Benchmarks.nullSafeDslPresent        thrpt   30  119.723 ± 0.506  ops/us\n[success] Total time: 3909 s, completed Feb 24, 2019 3:03:02 PM\n```\n\nYou can find the source code for the JMH benchmarks [here](https://github.com/ryanstull/ScalaNullSafe/blob/ebc0ed592fa5997a9c7b868cf8cdcea590e8ae07/benchmarks/src/test/scala/com/ryanstull/nullsafe/Benchmarks.scala#L18).  If you want to run the benchmarks yourself, just run `sbt bench`, or `sbt quick-bench` for a shorter run. These benchmarks\ncompare all of the known ways (or at least the ways that I know of) to handle null-safe traversals in scala.\n\nThe 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.\nBy leveraging the power of macros we are able to produce theoretically-optimal bytecode, whose performance is equivalent to the explicit null safety approach.\n\n## Why?\n\nSome 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]`. \nThe reason this library is needed is that there are situations where you need to extract deeply nested data, in a null-safe way, that was not defined using `Option[A]`. \nThis 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\ncode that dealt with extracting values out of highly nested [Avro](https://avro.apache.org/) data structures.\n\n## Notes\n\n* Using the `?` macro on an expression whose type is `\u003c: AnyVal`, will result in returning the corresponding java wrapper\ntype.  For example `?(a.getInt)` will return `java.lang.Integer` instead of `Int` because the return type for this macro must\nbe nullable.  The conversions are the default ones defined in `scala.Predef`\n\n* If you're having trouble with resolving the correct method when using the `?` macro with a default arg, try explicitly\nspecifying the type of the default\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanstull%2Fscalanullsafe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryanstull%2Fscalanullsafe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanstull%2Fscalanullsafe/lists"}