Series overview
Part 5 of 2025% complete
2026-04-05•25 min read

Option: absence as data

A citizen with no land-record entry is not a failure. The record may not exist yet, or the scheme may not need it — but null cannot tell “legitimately absent” apart from “someone forgot to set this field.” Option<T> makes absence a value with a type: Some<T> or None, and the compiler forces callers to handle both.

Yes, Optional exists. We build Option anyway — to see its implementation, to test its laws, and to give the library one consistent API (fold, Fn1-based mappers) across all its types. Part 19 covers when to use which.

The type

data/Option.java
public sealed interface Option<T> {
<R> Option<R> map(Fn1<? super T, ? extends R> mapper);
<R> Option<R> flatMap(Fn1<? super T, ? extends Option<R>> mapper);
Option<T> filter(Predicate<? super T> predicate);
<R> R fold(Supplier<? extends R> whenNone, Fn1<? super T, ? extends R> whenSome);
T orElse(T fallback);
T orElseGet(Supplier<? extends T> fallback);
static <T> Option<T> some(T value) {
return new Some<>(Objects.requireNonNull(value, "value must not be null"));
}
static <T> Option<T> none() {
return new None<>();
}
static <T> Option<T> ofNullable(T value) {
return value == null ? none() : some(value);
}
}

Some and None are records nested in the interface, so the sealed hierarchy closes itself — the permits list is inferred from the same file. some rejects nulls deliberately: an Option containing null recreates the exact ambiguity the type exists to remove.

The two implementations

data/Option.java
record Some<T>(T value) implements Option<T> {
@Override
public <R> Option<R> map(Fn1<? super T, ? extends R> mapper) {
return Option.some(mapper.apply(value));
}
@Override
public <R> Option<R> flatMap(Fn1<? super T, ? extends Option<R>> mapper) {
return mapper.apply(value);
}
@Override
public Option<T> filter(Predicate<? super T> predicate) {
return predicate.test(value) ? this : Option.none();
}
@Override
public <R> R fold(Supplier<? extends R> whenNone, Fn1<? super T, ? extends R> whenSome) {
return whenSome.apply(value);
}
// orElse/orElseGet return value
}
record None<T>() implements Option<T> {
@Override
public <R> Option<R> map(Fn1<? super T, ? extends R> mapper) {
return Option.none();
}
@Override
public <R> Option<R> flatMap(Fn1<? super T, ? extends Option<R>> mapper) {
return Option.none();
}
@Override
public Option<T> filter(Predicate<? super T> predicate) {
return this;
}
@Override
public <R> R fold(Supplier<? extends R> whenNone, Fn1<? super T, ? extends R> whenSome) {
return whenNone.get();
}
// orElse/orElseGet return the fallback
}

The asymmetry is the lesson: None.map does not call the mapper at all. Mapping over an absent value is a no-op, not an error — that is what makes chains of map safe to write without defensive null checks between steps.

Scheme example: optional land evidence

LandEvidence.java
// an existing evidence store returns null when no record exists — wrap at the boundary
Option<LandRecord> landRecord = Option.ofNullable(landRecords.get(citizenId));
Option<LandCategory> category = landRecord.map(LandRecord::landCategory);
String outcome = landRecord.fold(
() -> "no land record on file",
record -> "land category " + record.landCategory());

fold is the exit door: it collapses both cases to one type at the boundary where you must produce a response. Inside the pipeline you stay in Option; at the edge you fold out.

Choosing between Option, Result, and exceptions

  • Absent is a normal outcome → Option (no land record yet).
  • Absent carries a reason callers need → Result (Part 6) — a registry timeout is not the same as “not found,” and the type should say so.
  • The situation is genuinely unrecoverable — corrupted state, contract violation → an exception. Expected outcomes in disguised as exceptions make control flow invisible.

Law preview: Option obeys the functor and monad laws — map(identity) = identity, and some(a).flatMap(f) = f(a). Parts 7 and 10 test them for real; for now, hold the intuition that map never changes whether a value is present, only what a present value is.

JavaFunctional Programming

Type to search the site.

↑↓ navigate⏎ openPowered by Pagefind