The purpose of Null Tracking in general (of which Nullable Types are only one of many different forms), is to somehow regain a modicum of safety (and sanity) in languages that have null references.
If you have the chance to eliminate null references altogether, that is a much better solution since the problems that null references cause simply will not exist in the first place. Sir Tony Hoare has famously said that he considers inventing the Null Reference his "Billion Dollar Mistake", which is actually a quite conservative estimate on the total costs that null references have caused until today. If even the person who invented them considers them a mistake, why would you willingly put them in a language?
C# has them because, well, they probably didn't know any better, and now they can't get rid of them because of backwards-compatibility. TypeScript has them because its semantics are based on ECMAScript's, which has them.
The real beauty of an Option
type, though, is that it is isomorphic to a collection that can only hold from zero to one elements. Dealing with collections is one of the most important parts of programming, and thus every language in the world has powerful collections libraries. And you can apply all of the work that has gone into collections also to Option
types.
For example, if you want to execute an action with an option, you don't need to check whether it is defined! Every collection library on the planet has a way of iterating over a collection and executing an action for each element. Now, what does "executing an action for each element" mean for an Option
? Well, if there is no element, then no action is executed. And if there is one element, then the action is executed once with that element.
In other words, foreach
acts exactly like a NULL
check! You can just blindly do
mightExistOrMightNot.foreach(println)
and it will print out the value contained in the Option
if it exists and do nothing if it doesn't exist. The same applies when you want to perform a computation with the value. Every collections library on the planet has a way of iteration over a collection and transforming each element. Again, for an Option
"transforming each element" translates to "transform the value or do nothing". So you can just do
val squared: Option[Int] = mightExistOrMightNot.map(_ ** 2)
Also, collections libraries have ways to flatten nested collections. Imagine you have a long chain of references, each of which could be NULL
, and you wanted to access the last reference in that chain. With nested Option
s, you just write
longListOfReferences.flatten
And if you want to get a value out of an Option
, then you can simply write
mightExistOrMightNot.getOrElse(42)
and you will either get the value inside the option if it exists, or a default value of your choosing if it doesn't.
The only reason, really, for you to explicitly check for the existence of an Option
is if you want to do something completely different in case the value is missing.
It turns out that Option
is actually even more than "just" a collection. It is a monad. Languages like C#, Scala, and Haskell have built in syntax sugar for working with monads, and they have powerful libraries for working with monads. I will not go into details about what it means to be a monad, but e.g. one of the advantages is that there are some specific mathematical laws and properties associated with monads, and one can exploit those properties.
The fact that Java's Optional
is not implemented as a monad, not even as a collection, is a significant design flaw, and I think is partially to blame for people not understanding the advantages of Option
s, simply because some of those advantages cannot be realized with Java's Optional
.
There is also a more philosophical reason for choosing an Option
type over NULL
references. We can call this "language democracy". There is a major difference between those two: NULL
references are a language feature whereas Option
is a library type.
Everybody can write a library type, but only the language designer can write a language feature. That means that if for my code, I need to handle the absence of values in a slightly different manner, I can write a MyOption
. But I cannot write a MYNULL
reference without changing the language semantics and thus the compiler (or, for a language like C, C++, Java, Go, ECMAScript, Python, Ruby, PHP with multiple implementations, every single compiler and interpreter that exists, has existed, and will ever exist).
The more the language designer moves out of the language into libraries, the more the programmers can tailor the language (really, the library) to their needs.
Also, the more the language designer moves out of the language into libraries, the more the compiler writers are forced to make library code fast. If a compiler writer figures out some clever trick to make NULL
references fast, that doesn't help our hypothetical programmer who has written their own abstraction. But if a compiler writer figures out some clever trick to make Option
fast, it is highly likely the same trick will also apply to MyOption
(and Try
, Either
, Result
, and possibly even every collection).
Take Scala, for example. Unfortunately, because it is designed to interoperate and integrate deeply with the host environment (the Java platform, the ECMAScript platform, there is also an abandoned CLI implementation), it has null
references and exceptions. But, it also has the Option
type which replaces the former and Try
which replaces the latter. And Try
first appeared in a library of helpers released by Twitter. It was only later added to the standard library. Such innovation is much harder to do with language features.
I can write my own Scala Option
type, and I don't need to change the compiler for it:
sealed trait Option[+A] extends IterableOnce[A]:
override def iterator: Iterator[A]
override def knownSize: Int
def isEmpty: Boolean
def getOrElse[B >: A](default: => B): B
def foreach[U](f: A => U): Unit
def map[B](f: A => B): Option[B]
// … and so on
final case class Some[+A](value: A) extends Option[A]:
override def iterator = collection.Iterator.single(value)
override val isEmpty = false
override val knownSize = 1
override def getOrElse[B >: A](default: => B) = value
override def foreach[U](f: A => U) = f(value)
override def map[B](f: A => B) = Some(f(value))
// … and so on
case object None extends Option[Nothing]:
override def iterator = collection.Iterator.empty
override val isEmpty = true
override val knownSize = 0
override def getOrElse[B](default: => B) = default
override def foreach[U](f: Nothing => U) = ()
override def map[B](f: Nothing => B) = None
// … and so on
@main def test = Some(23).foreach(println)
Try it out here.
Option
consumes is inconsequential compared to the benefit it provides.Option
, you are doing it wrong.Option
is a container and thus iterable and traversable, it is also a monad, and thus also an applicative functor and a functor, which means it can be mapped and flatMapped over. You first example should bev.unwrap_or(0)
the second could (for example) simply be …enum
s to use empty niches in types. In particular, since references cannot be null in Rust,Option<&T>
will have the same size as a pointer.