17 February 2025
Hello [null]
In summary
null
is the bane of software engineering and has been called the “billion dollar mistake”- Many programming languages fail to alert the developer to potential
null
values, leaving the program vulnerable to runtime crashes - Borrowing from functional programming can help mitigate issues with
null
values
An introduction to options
In functional programming, an option is a special type used to represent data that may have missing values. To understand how options can help with null issues, we first need to understand a bit more about null
.
In programming, null
is a special term that effectively represents the lack of a value. Unfortunately, it can often be problematic, as attempting to use null
as a value where you are expecting data to exist will typically cause your program to crash. Because of this, it is often stated that null
pointers are a billion dollar mistake, and I completely agree with that assertion. However, the lack of a value is valid in many cases, so how can we represent this without all of the pain that comes with null
?
Before I answer that, let’s understand the state of things in most languages:
- Typically, there is some special syntax that denotes a type,
T
, may benull
. - Within Python, use the
typing.Optional[T]
type, or the newer union syntaxT | None
. - In C#, opt into the nullable suffix
T?
. - For other languages such as Go, use a pointer to represent an optional value with
*T
.
I find all of these quite manageable. C# will give you warnings if you haven’t checked that a value is present, either with the nullable operator or an explicit if check. Static analysis tools like mypy for Python tend to do a good job of this too, though it isn’t built in to the language itself. Go also has similar tools like golangci-lint to pick up the slack where the compiler doesn’t check.
Nullable values in practice
Let’s see what these look like in practice, without any 3rd party tools or configuration.
Python will crash at runtime:
name: str | None = None
name.upper()
# AttributeError: 'NoneType' object has no attribute 'upper'
C# gives a compiler warning when opting into the nullable suffix, but will not prevent you from crashing your program at runtime.
string? name = null;
name.ToUpper();
// Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
Go will crash at runtime, with no warning about a potential nil
dereference.
import "strings"
var name *string = nil
strings.ToUpper(*name)
// panic: runtime error: invalid memory address or nil pointer dereference
The alternative
Given that many languages require either some level of configuration or a 3rd party tool to help with null
checking, what if it was impossible to miss a potential null
value?
Let me introduce the Option
(or Maybe
) type from our friends over in the functional programming world. This is a special type used to represent data that may have missing values. Option
has two possible states, which represent whether a value exists, or not.
Some(T)
andNone
How are they different to nullable values?
Nullable values can often be incorrectly accessed, and with no warnings or errors from the compiler until your program crashes. With an Option
, the value you have is wrapped in another type, so you are unable to access the inner value without either checking the value exists, or explicitly pulling the value using something like Rust’s Option::unwrap
, which will crash the program when the value is not present. I personally prefer this as the explicitness makes it extremely obvious as to what is happening when reading and writing code.
Let’s revisit at the first example again, but with a language that implements the Option
type, Rust.
let name: Option<&str> = None;
name.to_uppercase();
// error[E0599]: no method named `to_uppercase` found for enum `Option` in the current scope
The compiler fails citing that to_uppercase
doesn’t exist on the Option
type. This means we cannot accidentally directly access the underlying value in an Option
in the same way that you can dereference a null
pointer.
Using an Option
Since we can’t just use T
when it’s wrapped in an Option
, we must first unwrap T
. There are various methods to do so, which you choose will depend on the situation.
You can use Option::unwrap
in Rust’s implementation, which will take the inner value, but crash at runtime if that value is None
. This can be mitigated using calls to Option::is_some
or Option::is_none
to help with control flow, but there are better ways.
Rust in particular offers if let
expressions, that allow you to assign a variable and execute a block of code if a condition matches. The below will run n.to_uppercase()
if name has a value.
let name: Option<&str> = None;
if let Some(n) = name {
n.to_uppercase();
}
Option
s also feature a lot of helper methods, such as map
, can be used to apply a function to the Some(T)
variant of an Option
, or unwrap_or
, which will give you the current value in the Option
, or the provided default value if the Option
is None
.
let name: Option<&str> = None;
let mapped: String = name.map(str::to_uppercase).unwrap_or("No name provided".into());
The Python equivalent for this code would be.
name: str | None = None
mapped: str = name.upper() if name is not None else "No name provided"
While the Python version is more concise, there is nothing built in to warn you that you’d even need the if ...
in the first place.
So, why use an Option?
Option
s require you to access the value through verifying its state, or explicitly pulling the value and accepting a program crash if it is not there. This means compilers are able to catch misuse much easier than nullable types or pointers, leading to safer and more reliable code overall.
Get in touch
Get in touch with Louder or sign up to Louder’s newsletter to receive our articles and the latest industry updates straight to your inbox.