The purpose of option types is to let the compiler know exactly where a value may be empty in a language where null simply does not exist (typical for any language that supports algebraic data types). This way, the compiler can give compiler-time errors if you forget to check for emptyness. Here's an example in Rust, an imperative language with a C-like syntax:
let my_number : Option<int> = read_number();
return 2 * my_number; // compile error
This won't compile because my_number
does not contain an int
but an Option<int>
(i.e. an int
that may be empty). The compiler does not allow multiplication on Option<int>
.
Therefore, it is necessary to pattern match my_number
in order to extract its value:
let my_number : Option<int> = read_number();
match (my_number) {
Some(my_actual_number) =>
return Some(2 * my_actual_number); // ok
None ->
return None; // fallback if it's empty
The pattern matching forces the programmer to handle every possible scenario: in this case, the None
case. This is how it prevents the typical "forgot to check for null" problem.
However, in a language where null is pervasive, option types aren't nearly as useful since everything can already be null anyway. Therefore the compiler can't really help you (since inferring the nullability would require solving the halting problem).
The null object pattern not the same thing, but it is a related concept.
In languages that have first-class support for option types, one is permitted to apply an operation to the internal object inside the option type, if it exists. It serves as a shortcut for the common activity of "do X to the object, but if it's empty then forget about it". This process is typically called "mapping" and this operation can be defined for all option types.
The map operation takes an option object (Option<T>
), as well as a function that can act on the internal object (T -> R
), and produces the result of the function wrapped in an option object (Option<R>
).
In fact, the example above can be rewritten much more succinctly using a map combined with an anonymous function:
let my_number : Option<int> = read_number();
return my_number.map(
// use anonymous function to specify what to do
|my_actual_number| 2 * my_actual_number
);
Map can be thought of as a recipe that:
- If the option object is not empty, then apply the provided function and return the result in a
Some(...)
.
- If the option object is in fact empty, then do nothing and just return
None
.
In the null object pattern, one uses a "null" class that possesses same methods as the non-null class (they could be siblings in the inheritance hierarchy), but arranged in such a way that the operations on the "null" class simply do nothing. Hence, the null object pattern is similar in that it accomplishes this same goal, albeit in a different, more object-oriented/duck-typing way.
Null Object
andMaybe
are in some sense exact opposites:Maybe
forces the client to explicitly deal with a potentially absent value, whereasNull Object
makes the client completely ignorant about the fact that a value might be missing.