Motivation
Scala3’s powerful additions enable new (potentially better) ways to think about existing patterns. One such pattern is the handling of potentially missing values, for which the best principled approach has historically been to use the Option monad, which certainly has its merits:
- Option forces the programmer to consider and deal with the potentially missing case
- it provides a rich and natural API to do so
Nevertheless, with Scala3’s union types and explicit nulls, the union T | Null emerges as a principled alternative to deal with this pattern. While it keeps Options hygiene of explicitly handling the absence of values, it distinguishes itself with clear advantages:
- better runtime efficiency as values are unboxed
- clearly improved user experience
The second point maybe deserves some explanation. Our APIs quite often have to provide methods with lots of optional values, for which the default is to not pass any value. The reduction in syntax overhead of not having to wrap every value that you actually want to pass with Some(_), essentially going from something like this:
generate(
model = "mymodel",
prompt = Some("Nullable types are"),
suffix = Some("alternative to Options"),
options = Some(Options(seed = Some(12), temperature = Some(0))),
think = Some(true),
)
to this
generate(
model = "mymodel",
prompt = "Nullable types are",
suffix = "alternative to Options",
options = Options(seed = 12, temperature = 0),
think = true
)
doesn’t only make code much easier to write and to read, but also matches my expectations as a user better of just passing values as is when they’re present.
But despite its benefits, T | Null has one clear drawback compared to Option: it doesn’t enjoy the same rich and fluent API.
Proposal
The proposal is quite simple (both to express and to implement
). It’s NOT not about bringing any changes or additions to the languages or its syntax. Rather, it’s to continue what has been started in Working with Null and provide additional extensions to make working nullable values easier and more fluent. More concretely, for example:
extension [T](x: T | Null)
def map[U](f: T => U): U | Null = if x == null then null else f(x)
def fold[U](ifEmpty: => U)(f: T => U): U = if x == null then ifEmpty else f(x)
...
The general idea then would be to provide pretty much a mirror of the Option API and all its methods.
While this arguably could be implemented for one own’s code (which I have personally done) or provided as a user library, the overhead of the former to take into every project you’re working on and the non-standard nature of the latter make it hard to provide an API to user relying on nullable types.
Closing remarks
A potential argument against encouraging the use of nullable types as T | Null is the inability to model nested options. However, arguably in more than 90% of cases you don’t need such a capability. And if you do, you can still use Option.
All in all, every approach to handling missing values comes with trade-offs, and this is certainly the case for both T | Null and Option. But despite their limitations, nullable types combined explicit nulls have certainly become a principled and viable alternative with clear advantages in many cases. This proposal will then enable taking advantage of their benefits with the powerful and familiar API users know and love from Option.