Skip to content

Rationale · essay 06

Absence

rationale.md · 75 lines · 3 min read

The redefinition: no null, no implicit zero-value. Three concepts that other languages confuse stay separate: absence, default and null.

“What if there is no value?” is one of the oldest questions in programming, and the most common answer, the null/nil/NULL, is admittedly the “billion-dollar mistake”: a value that inhabits any type and blows up on access. Go softens it with the zero-value (every type has an implicit default), but it trades one problem for another.

Three concepts, three distinct answers, with no overlap:

  1. Absence = Optional[T], with the builtin value none. The type says it may be missing. You unpack it with match or with the honest API (is_present/or_panic/or). A pointer is guaranteed valid; “there may be no pointer” is Optional[*T].
  2. Default = explicit RHS. Every binding is born with a value that you write (x := 0, var name := ""). 0 and "" only appear when you type them.
  3. Null = does not exist. There is no “this is nothing” sentinel inhabiting the types.

And the case that seems to contradict but confirms: the Value.Null of JSON/binary (encoding ch.). It is a token of the format (the null literal that JSON has), not the language’s null. It is a variant present and normal in an enum; it maps to Optional.none only at the serialization boundary. Three concepts, and not even the format token breaks the rule.

The separation attacks a specific ambiguity of the zero-value: “is it 0 because someone asked for 0, or because nobody initialized it?” In Go, var x int is 0 and var p *T is nil without you asking, and then there is no way to distinguish “missing” from “is the default”. Makoto closes the door: since every binding has an RHS, a 0 is always intentional, and absence is always an explicit Optional. The entire class of bugs “I forgot to initialize” disappears because there is nothing to forget; you do not write an empty variable.

This fits into the pointer model (ch. 02): the non-nullable pointer is only possible because absence lives in a separate type (Optional[*T]). And it fits into the error model (ch. 05): an allocation that can fail does not return a silent null pointer; it returns a Result with an error. Absence, default and failure are three things, and each one has its typed form.

  • Implicit null/nil (C/Java/Go-pointers). It inhabits every type, blows up on access, and does not appear in the signature. Optional[T] puts the possibility in the type and forces the unpacking.
  • Zero-value (Go). It solves “always has a value” but creates the ambiguity “0 or missing?”. The mandatory RHS gives “always has a value” without the ambiguity, because the value is always chosen.
  • Coercion by omission (an omitted field becomes an invisible default, C++). It reopens the “value that nobody asked for”. In Makoto, omitting is an error; you write what you want.
  • nil, nil as a return (Go). “Is it nil because it does not matter, or because it failed?” Ambiguous. Result[(...), E] separates: success is Ok, failure is Err; one does not disguise itself as the other.

The NullPointerException/segfault that rises from a field nobody initialized. The Go struct you think is filled and is full of silent zeros. The repeated if (x === null || x === undefined) in JavaScript, where two different “nothings” do the same thing. Each one is the confusion between absence, default and null charging the price.

Absence is typed; default is written; null does not exist. If something can be missing, the type says so (Optional[T]). If it has an initial value, you write it. There is no third magic case.

The price is verbosity in the RHS: you initialize everything, and sometimes you write a row of x := 0; y := 0. And Optional has to be unpacked at every use, instead of being read as if it were the value. More boilerplate, in exchange for guaranteed safety at compile time: absence never catches you by surprise, because it is always written in the type.

Next: 07 · Types