Null, None, and Nothing: how eight languages handle absence
Six languages, one question: when a value is missing, does the type system know, or does the program find out at 3am?
Every program eventually has to say "there is nothing here." A lookup misses, a field is unset, a parse fails. The interesting question is not whether a language can represent absence - they all can - but whether absence is a value the type system tracks or a hole the runtime stumbles into. Tony Hoare called his 1965 null reference his "billion-dollar mistake," and the languages below sort neatly into those that repeated it and those that designed it out. The clean dividing line is this: can you reach an absent value without the compiler having forced you to consider that it might be absent?
Go: zero values and the nil that lurks
Go has no sum-typed Option. Absence is expressed two ways, and neither is fully type-checked. The first is the zero value: every type has one, and a missing int is silently 0, a missing string is "". The second is nil, the absent pointer, slice, map, channel, interface, or function.
m := map[string]int{"ada": 30}
v := m["nobody"] // v == 0, with no signal that the key was missing
v, ok := m["nobody"] // the "comma-ok" idiom: ok == false
The comma-ok form is Go's honest answer, but it is opt-in: you can always drop the ok and accept the zero value, conflating "present and zero" with "absent." Worse, nil is not tracked in the type. A *User may be nil, and dereferencing it is a runtime panic, not a compile error. Go's interface-nil corner (a non-nil interface wrapping a nil pointer) is a famous gotcha. Go's bet is simplicity over guarantees: absence is conventional, and discipline (and go vet) does the work the type system declines to.
Rust: there is no null
Rust removed null entirely. Absence is Option<T>, an ordinary enum, and fallibility is Result<T, E>. Because Option<T> and T are different types, you cannot use a possibly-absent value where a definite one is required without first handling the gap. The compiler is the enforcement.
let xs = vec![10, 20, 30];
match xs.get(99) {
Some(v) => println!("got {v}"),
None => println!("absent"),
}
let total = xs.get(99).copied().unwrap_or(0); // 0
The ? operator threads early returns through Option and Result, so the happy path reads linearly while every failure still flows out explicitly. Crucially, Option<&T> carries no space overhead for reference types thanks to the null-pointer optimization: the "billion-dollar mistake" survives only as a bit pattern, never as a reachable null. The cost is that absence is unforgeable but also unignorable - you handle it or you do not compile.
Haskell: Nothing as a monad
Haskell, pure and lazy, models absence as Maybe a, with constructors Just a and Nothing. Like Rust's Option, it is a plain algebraic data type with no privileged status, and there is no null. What distinguishes Haskell is that Maybe is a Monad, so absence composes without manual unwrapping.
lookupAge :: String -> Maybe Int
lookupAge name = lookup name [("ada", 30)]
bump :: String -> Maybe Int
bump name = do
age <- lookupAge name -- short-circuits to Nothing on a miss
pure (age + 1)
The do block above is Rust's ? written as sequencing: the first Nothing aborts the chain. The same machinery (>>=, fmap, <|>) drives Either e a for errors that carry a reason. Type classes mean the absence-handling vocabulary - maybe, fromMaybe, traverse - generalizes across every monad, not just Maybe. Laziness adds a subtler wrinkle: a value can be present but undefined (a "bottom," undefined or a non-terminating thunk), which Maybe does not capture. So Haskell banishes null but keeps a smaller ghost: forcing a bottom still throws. (This snippet is illustrative; GHC is not installed in this environment, so it is shown from knowledge rather than executed.)
OCaml: the option that started the family
OCaml's 'a option, with Some x and None, is the direct ancestor of Rust's and the cousin of Haskell's Maybe. It is a variant type, matched exhaustively, and the compiler warns when a match forgets a case - so a forgotten None is caught at compile time.
let lookup_age tbl name =
match List.assoc_opt name tbl with
| Some age -> age
| None -> 0
let total = Option.value (List.nth_opt xs 99) ~default:0
OCaml's standard library has steadily migrated toward _opt variants (List.nth_opt, Hashtbl.find_opt) precisely to replace functions that once raised Not_found. The instructive contrast with Rust is that OCaml does still have escape hatches: List.assoc (no _opt) raises an exception, and Option.get None raises Invalid_argument. Absence is type-tracked when you opt into the option-returning API, but exceptions remain a parallel channel. OCaml shows the type-safe path and the legacy path side by side. (Not executed here; shown from knowledge.)
Python: None, the singleton you can pass anywhere
Python takes the dynamic-language route. None is a single object of type NoneType, and it can inhabit any variable, because variables are untyped at runtime. This is null wearing a friendlier face: there is exactly one None, compared with the idiomatic is, and it never silently becomes 0.
d = {"ada": 30}
d.get("nobody") # None
d.get("nobody", 0) # 0, an explicit default
x = None
if x is None: ... # identity, not equality
I ran this to confirm: d.get("nobody") is None, and None is None is True. The trap is truthiness. Because 0, "", [], and None are all falsy, if not value: cannot distinguish "absent" from "present but empty," which is why is None is the correct test. Modern Python pushes back with typing.Optional[T] (that is, T | None), which static checkers like mypy enforce even though the interpreter ignores it. So Python's absence story is bimodal: a permissive runtime where None goes anywhere, layered with optional static types that recover much of what Rust and OCaml get for free.
Guji: absence is a value, never a hole
Guji, an in-house statically-typed functional language, sits firmly in the no-null camp and, like Rust, makes that the only camp. The language has no null and no exceptions at all; per its specification, "operations that may be absent or may fail return one of two standard sum types," Option[T] and Result[T, E]. Indexing a list out of bounds panics and aborts the process; the checked alternative, get, returns Option[T] instead. The choice is explicit in every signature.
The following runs on the guji v0 tree-walking evaluator (verified in this environment):
sub describe($o: Option[Int]): Str {
match $o {
Some($v) { "some: $v" }
None { "none" }
}
}
sub main(): Int {
@xs = [10, 20, 30]
print(describe(@xs.get(1))) # some: 20
print(describe(@xs.get(99))) # none
$total = @xs.get(99).unwrap_or(0)
print("default total: $total") # default total: 0
0
}
Guji folds the family's lessons together. Its match is exhaustive: the compiler rejects a match that forgets None, so OCaml's missing-case warning becomes a hard error. Its postfix ? operator is Rust's and Haskell's short-circuit, propagating a None or Err out of the enclosing function - I confirmed that safe_div($a, 0)? returns early with None rather than dividing. And its sigils ($x scalar, @list, %map) and immutable-by-default bindings keep absence a transformed value rather than a mutated one: the Option toolkit (unwrap_or, map, and_then, ok_or) lets you chain past a gap without ever touching a raw null. Even guji's channels lean on it - a receive yields Option[T], with None meaning "closed and drained," folding Go's "value, ok" pair into the one absence type.
The dividing line
Two philosophies emerge. Go and Python keep a reachable absent value - nil, the zero value, None - that the runtime, not the compiler, polices; they buy flexibility and pay in panics and is None discipline. Rust, Haskell, OCaml, and guji make absence a distinct type you cannot accidentally use as a present value; they buy guarantees and pay in ceremony, though ? and do-notation make the ceremony nearly invisible. Larry Wall's line that we should "make the easy things easy and the hard things possible" lands differently on each side: the typed languages make handling absence easy and mandatory, while the dynamic ones make ignoring it easy - which is exactly the temptation Hoare warned us about. Whichever side a language picks, the honest measure is the same: when the value is missing, who finds out, and when.