null Is a Pattern in a match Over a T?Status: Accepted
Date: October 14, 2026
Answers: how a match takes a T? apart (issue #612)
Related:
ADR-279 (nullable types);
ADR-291 (patterns, else, guards);
ADR-382 (a test that proves a T? is not null);
Part I 3.4; Part III 17.1
Can a match name the empty case of a T?? A T? is a T or nothing
(Part I 2.3). ??, ?. and if x != null take it apart where one case is
special; where the nothing and several values of the T each have their own
answer, the program needs a match, and a match had no pattern for the
nothing.
null is a pattern; the other arms match the T.In a match over a T?, null is the arm taken where the value is not set.
Where a match has a null arm, every other arm is matched against the T
inside: a bare name binds the plain T, a literal compares with the value
(0 => over an i64?), a variant names one of the T’s variants, and a
guard works as on any arm (null if c =>).
match count {
null => "unknown",
0 => "none",
n if n > 9 => "many",
n => f"{n}",
}
A position of a tuple narrows on its own (#879, owner, 2026-10-27). In
a match over a tuple, a bare name at a position whose type is a T? binds
the T once the arms before it have taken every value with null at that
position that its own arm would match: an unguarded earlier arm has null
there, and at each other position either _ or a bare name, or the same
null, variant or literal this arm has there (a literal added by #890,
architect, 2026-10-27: like a variant it is one fixed value, so the earlier
arm took null at the position for exactly the values this arm meets:
after (false, null), (false, why) binds why as the T). A variant or a literal at a T?
position ((Color::Red, n)) meets only set values and is matched against the
T. A tuple is matched as a struct whose fields are its positions.
match (a, b) {
(null, null) => "none",
(null, y) => f"b {y.len()}", // `y` is a `String`: `(null, null)` came first
(x, _) => f"a {x.len()}", // `x` is a `String`: `(null, y)` took the rest
}
Without such an arm the name binds the T? as before: after (null, null)
alone, (x, _) still meets (null, "b"). A guarded arm before takes
nothing (D2), and no new pattern says “set”: a guard that proves it
((x, _) if x != null, ADR-382 D2) or the null arms before say it.
Why. The principle of ADR-382 D1: where the compiler has proved the value is
set, it is a T. An arm after the nothing has been given its own arm meets
only values, so the name it binds is a value and needs no ??.
null arm and a catch-all are a complete match.Over a T? with a null arm, the nothing is covered by an unguarded null
arm or by else, and the rest is counted against the T as any match over
a T is: an unguarded bare name covers it, an enum’s variants are counted,
true and false cover a bool. A guarded null arm covers nothing, as no
guarded arm does (ADR-291 D11). What is missing is NK1151.
Why. The same rule as every other match, with one case added to it: no
second notion of completeness.
null over a value that is never null is refused (NK1256).A null arm in a match over a type that is not a T? can never be taken,
and is refused rather than ignored. Where the subject’s type is not known,
nothing is claimed.
Why. An arm that cannot run is a misreading of the type, and it would also change what the other arms bind.
None and Some(…).null is None; every other arm but else is Some(<pattern>). In a
tuple, null at a T? position is None, and a position D1 matches
against the T is Some(<pattern>).
Some and None are not Nikaia; a pattern that names them is refused.A T? has no constructors in the language (ADR-278 D5): its nothing is
null, its value is the T itself. So Some(x) and None are not patterns
of a T?, whatever arms stand beside them; the compiler refuses each with its
own code and the help writes the Nikaia form (Some(x) => is x =>,
None => is null =>). Over a type of the program’s own that declares a
variant called Some or None, the name is that variant’s, as any other.
Why. A pattern the language does not have, taken as an unknown variant,
binds an untyped name and passes or fails by accident (Some(x) beside
else was accepted, beside null refused as incomplete). One way to write the
value of a T? in a match, the one D1 gives.
B: nullables stay out of match, taken apart only with ??, ?. and
if. Where three or more cases meet - nothing, a special value, the rest -
it nests an if around a match over x ?? <a value that cannot occur>,
whose fallback is dead code that reads as if it mattered (ADR-382, option A).