Option[T] has exactly two variants: Some(T) when a value is present and
None when it is absent. It is an ordinary enum declared in the prelude, so
everything you already know about matching applies to it.
fn describe(port: int?): string {
return match port {
Some(value) => "listening on ${value}"
None => "not listening"
}
}
fn main(): void {
println(describe(Some(8080)))
println(describe(None))
}This page covers what Option means in the language: how absence is spelled,
which operators are built around it, and when to convert it into a Result.
The complete method catalogue — every signature, with a worked example each —
lives in Standard library › Option.
T? and Option[T] are the same type. The suffix form is the preferred
spelling in signatures and annotations.
fn main(): void {
long: Option[string] = Some("Atoll")
short: string? = Some("Atoll")
println("${long == short}")
}There is no null
Atoll has no null literal. A value may be absent only when its type says so,
and the checker enforces that at every assignment:
fn main(): void {
name: string = None
println(name)
}That is rejected with ATOLL2002: expected string, found ?…? — None has an
optional type and string does not. Widen the annotation instead:
fn main(): void {
name: string? = None
println(name ?? "anonymous")
}The same rule shows up wherever the standard library returns an Option.
Indexing a list is one of those places — xs[0] and xs.get(0) both produce
int?, never int:
fn first(xs: []int): int {
return xs[0]
}fn first(xs: []int): int {
return xs[0] ?? 0
}
fn main(): void {
println("${first([7, 8, 9])}")
println("${first([])}")
}Construction
Some(value) and None are the constructors. An expected optional type also
lifts a bare value, which keeps struct literals and returns readable:
struct Server { host: string, port: int? }
fn main(): void {
explicit: int? = Some(8080)
lifted: int? = 8080
absent: int? = None
s := Server { host: "localhost", port: 443 }
total := explicit.unwrap_or(0) + lifted.unwrap_or(0)
+ absent.unwrap_or(0) + s.port.unwrap_or(0)
println("${total}")
}None carries no payload, so nothing in the expression itself identifies T.
The expected type has to supply it — from an annotation, a parameter, a field,
or a return type.
Nested optionals are distinct types. string?? is Option[Option[string]] and
can tell “no outer answer” apart from “an outer answer that contains nothing”:
fn lookup(present: bool): string?? {
if not present { return None }
return Some(None)
}
fn describe(r: string??): string {
return match r {
None => "no answer"
Some(inner) => match inner {
None => "answered: no value"
Some(v) => "answered: ${v}"
}
}
}
fn main(): void {
println(describe(lookup(false)))
println(describe(lookup(true)))
}Reach for a nested option only when the two levels genuinely mean different things. Otherwise flatten it or model the states with a named enum.
Matching
Both cases must be covered. Leaving one out is a checker error, not a runtime surprise:
fn f(v: int?): int {
match v {
Some(x) => { return x }
}
}That reports ATOLL2010: non-exhaustive match on option — missing: None.
Patterns nest, so an option wrapping an enum destructures in one arm:
enum Shape {
Circle(float)
Square(float)
}
fn area(s: Shape?): float {
match s {
Some(Circle(r)) => { return 3.14159 * r * r }
Some(Square(w)) => { return w * w }
None => { return 0.0 }
}
}
fn main(): void {
println("${area(Some(Shape.Square(3.0)))}")
println("${area(None)}")
}When only the Some branch is interesting, an if pattern is shorter than a
full match:
fn name_of(cache: Map[int, string], id: int): string {
if Some(name) := cache.get(id) {
return name
} else {
return "miss"
}
}
fn main(): void {
mut cache: Map[int, string]
cache.put(1, "ada")
println(name_of(cache, 1))
println(name_of(cache, 2))
}A refutable binding with else keeps the payload in scope for the rest of the
function. The else block must diverge — return, error, break, or
continue.
fn initial(cache: Map[int, string], id: int): char? {
Some(name) := cache.get(id) else {
return None
}
return name.char_at(0)
}
fn main(): void {
mut cache: Map[int, string]
cache.put(1, "ada")
println("${initial(cache, 1).is_some()}")
println("${initial(cache, 2).is_some()}")
}Fallback: ?? and ??=
?? produces the payload, or evaluates its right-hand side when the left is
None. The right side does not run for Some.
fn label(configured: string?): string {
return configured ?? "untitled"
}
fn main(): void {
println(label(Some("prod")))
println(label(None))
}The right operand of ?? must be the unwrapped type, so ?? does not chain
across several options. Combine options with .or(...) first and let ??
close the chain:
fn pick(a: string?, b: string?): string {
return a ?? b ?? "fallback"
}fn pick(a: string?, b: string?): string {
return a.or(b) ?? "fallback"
}
fn main(): void {
println(pick(Some("a"), Some("b")))
println(pick(None, Some("b")))
println(pick(None, None))
}??= fills a mutable optional place only when it is currently absent. A second
??= on the same place is a no-op:
struct Config { name: string? }
fn main(): void {
mut c := Config { name: None }
c.name ??= "default"
c.name ??= "ignored"
println(c.name ?? "")
}Safe access: ?.
?. reads a field or calls a method only when the receiver is Some, and
produces None otherwise. The result stays optional.
struct Profile { city: string }
struct Account { profile: Profile? }
fn city(a: Account?): string? {
return a?.profile?.city
}
fn main(): void {
resident := Account { profile: Some(Profile { city: "Oslo" }) }
nomad := Account { profile: None }
println(city(Some(resident)) ?? "unknown")
println(city(Some(nomad)) ?? "unknown")
println(city(None) ?? "unknown")
}Methods work the same way:
struct User {
name: string
fn greet(self): string => "hi ${self.name}"
}
fn greeting(u: User?): string? {
return u?.greet()
}
fn main(): void {
println(greeting(Some(User { name: "ada" })) ?? "nobody")
println(greeting(None) ?? "nobody")
}?. lifts exactly one access. The member it produces is optional again, so a
following plain . sees an Option and fails:
struct User { name: string }
fn shout(u: User?): string? {
return u?.name.to_upper_ascii()
}Use map when the whole chain should run against the payload:
struct User { name: string }
fn shout(u: User?): string {
return u.map(x => x.name.to_upper_ascii()) ?? "NONE"
}
fn main(): void {
println(shout(Some(User { name: "ada" })))
println(shout(None))
}The four optional operators have distinct control behaviour:
| Form | On Some(value) |
On None |
|---|---|---|
opt?.field |
access the field, re-wrap in Some |
produce None, continue |
opt? |
produce value |
return None from the enclosing optional function |
opt ?? fallback |
produce value |
evaluate and produce fallback |
place ??= value |
leave the place unchanged | evaluate and store value |
Safe access is a transformation, propagation is an exit, and coalescing is recovery. Pick the one that matches the boundary you are standing on.
Propagation
Postfix ? produces the payload and returns None from the enclosing
function. That function’s success type must itself be optional.
struct User { name: string, manager: int? }
fn manager_name(users: Map[int, User], id: int): string? {
user := users.get(id)?
boss := users.get(user.manager?)?
return Some(boss.name)
}
fn main(): void {
mut users: Map[int, User]
users.put(1, User { name: "ada", manager: Some(2) })
users.put(2, User { name: "grace", manager: None })
println(manager_name(users, 1) ?? "none")
println(manager_name(users, 2) ?? "none")
println(manager_name(users, 3) ?? "none")
}? on an option inside a non-optional function is rejected:
fn f(xs: []int): int {
v := xs.get(0)?
return v
}The message is ATOLL2002: ? on an option requires an enclosing function that returns an option. It does not silently invent an error for a T ! E
function either — convert absence explicitly with to_result, below.
Propagation covers the full rules for ? on
both carriers.
Absence is not failure
The one conversion that belongs on this page is the one that changes the
meaning of a value. to_result(err) says “at this boundary, absent means
this failure”:
error LookupError { Missing { key: string } }
fn require(settings: Map[string, string], key: string): string ! LookupError {
missing := LookupError.Missing(key)
return settings.get(key).to_result(missing)?
}
fn main(): void {
mut settings: Map[string, string]
settings.put("host", "localhost")
match require(settings, "host") {
Ok(v) => println("host=${v}")
Err(e) => println("missing host")
}
match require(settings, "port") {
Ok(v) => println("port=${v}")
Err(e) => println("missing port")
}
}The error argument is an ordinary call argument, so it is constructed before
the method runs even for Some. When building it is expensive, use a match.
Note the intermediate missing binding: a variant payload written with braces
cannot appear directly in an argument list, because the parser reads the { as
a block. Bind it first, or use the positional constructor
LookupError.Missing("port"). See
Error Types.
Going the other way, Result.ok() throws the reason away and leaves you with
an Option — deliberate loss of detail, never a default.
The method surface
Every Option method is total; none can panic. They fall into five groups:
| Group | Methods |
|---|---|
| Query | is_some, is_none, is_some_and |
| Discharge | unwrap_or, unwrap_or_else |
| Transform | map, flat_map, filter |
| Combine | or, or_else, and, zip |
| Convert | to_result, to_list, flatten |
Signatures, semantics, and a worked example for each live in Standard library › Option. Two points are worth carrying here because they change program behaviour:
- the
_or_elseforms take a zero-argument function, and only call it forNone; the plain forms take an already-evaluated argument; flattenis declared onOption[T]returningOption[T], so on aT??receiver it does not remove a layer at the type level. Useflat_map(v => v)to collapse a nested option.
fn expensive_default(): int {
return 42
}
fn main(): void {
v: int? = None
println("${v.unwrap_or(0)}")
println("${v.unwrap_or_else(() => expensive_default())}")
nested: int?? = Some(Some(7))
flat: int? = nested.flat_map(inner => inner)
match flat {
Some(n) => println("collapsed to ${n}")
None => println("collapsed to nothing")
}
}A composed example
Reading configuration is the archetypal Option workload: every lookup can be
absent, some absences are fatal and some have defaults.
struct Settings {
host: string
port: int
workers: int
tls_cert: string?
}
error ConfigError {
Missing { key: string }
NotAnInteger { key: string, raw: string }
}
fn required(raw: Map[string, string], key: string): string ! ConfigError {
missing := ConfigError.Missing(key)
return raw.get(key).to_result(missing)?
}
fn integer(raw: Map[string, string], key: string, fallback: int): int ! ConfigError {
text := raw.get(key)
Some(value) := text else {
return fallback
}
bad := ConfigError.NotAnInteger(key, value)
return value.to_int().to_result(bad)?
}
fn load(raw: Map[string, string]): Settings ! ConfigError {
return Settings {
host: required(raw, "host")?,
port: integer(raw, "port", 8080)?,
workers: integer(raw, "workers", 4)?,
tls_cert: raw.get("tls_cert"),
}
}
fn main(): void {
mut raw: Map[string, string]
raw.put("host", "localhost")
raw.put("workers", "8")
match load(raw) {
Ok(s) => println("${s.host}:${s.port} x${s.workers} tls=${s.tls_cert.is_some()}")
Err(e) => println("bad config")
}
mut broken: Map[string, string]
broken.put("port", "8080")
match load(broken) {
Ok(s) => println(s.host)
Err(e) => println("bad config")
}
}tls_cert stays an Option because absence is legitimate data. host becomes
an error because a missing host is a failure the caller must hear about. That
choice — absence versus failure — is the whole design decision, and it belongs
at the boundary that knows the meaning.
Boundaries
An optional field is a claim that absence is valid data. Document what produces
None, whether it can later become Some, and whether omission differs from
an empty value:
Noneis not"",0,false, or[];- a nested option is not automatically flattened;
- converting to
Resultpicks the layer that owns the failure meaning; - discarding a
Resulterror with.ok()is a deliberate loss of detail.
The compiler may niche-pack an optional reference or use an explicit tag for an
inline scalar. That representation never changes the source cases: every
Option[T] is still exactly Some(T) or None, and code must not infer
presence from a raw address or integer value.
Do not add ? to every field “for flexibility”. Optionality propagates into
construction, matching, serialization, SQL nullability, and every caller’s
invariants.