Option[T] models a value that may be absent. It has exactly two variants, and
the shorthand T? means the same thing.
fn lookup(id: int): string? {
if id == 1 { return Some("ada") }
return None
}
fn main(): void {
println("${lookup(1) ?? "unknown"}")
println("${lookup(9) ?? "unknown"}")
}Atoll has no null, so Option is how absence is expressed everywhere: a
missing map key, an out-of-range list index, an unset struct field, a search
that found nothing. Every Option method is total — the prelude deliberately
has no unwrap() or expect() that can abort.
fn main(): void {
value: int? = Some(3)
n: int = value.unwrap()
println("${n}")
}An Option[T] also never coerces to T. The absence has to be discharged
explicitly:
fn main(): void {
value: int? = Some(3)
n: int = value
println("${n}")
}Where options come from
Most Option values in a program are produced by the prelude rather than
written by hand.
fn main(): void {
values := [10, 20, 30]
// Indexing and `get` are bounds-checked, so both return `int?`.
a: int? = values.get(0)
b: int? = values[9]
println("${a ?? -1} ${b ?? -1}")
// Searches report "not found" as None.
println("${values.find(v => v > 15) ?? -1}")
println("${values.first() ?? -1} ${values.last() ?? -1}")
println("${values.min() ?? -1} ${values.max() ?? -1}")
// Missing map keys.
mut ports: Map[string, int] = Map.new()
println("${ports.get("http") ?? 0}")
}Inspection
is_some, is_none, and is_some_and answer boolean questions without
extracting anything.
struct Item { active: bool, id: int }
fn main(): void {
present: Item? = Some(Item { active: true, id: 7 })
absent: Item? = None
println("${present.is_some()} ${present.is_none()}")
println("${absent.is_some()} ${absent.is_none()}")
// The predicate runs only on the Some side; None answers false.
println("${present.is_some_and(item => item.active)}")
println("${absent.is_some_and(item => item.active)}")
}Getting the value out
There are four ways, and they differ in what happens on None.
fn expensive_default(): string {
println("computing default")
return "computed"
}
fn main(): void {
label: string? = None
// 1. The `??` operator — shortest form for a constant fallback.
println("${label ?? "untitled"}")
// 2. `unwrap_or` — same thing as a method; the argument is eager.
println("${label.unwrap_or("untitled")}")
// 3. `unwrap_or_else` — the closure runs only when the value is absent.
println("${label.unwrap_or_else(() => expensive_default())}")
// 4. `match` — the only form that lets the two cases do different work.
match label {
Some(text) => println("have ${text}")
None => println("nothing to show")
}
}Reach for unwrap_or_else whenever the fallback allocates, performs I/O, or
calls user code — unwrap_or’s argument is evaluated before the call, present
or not.
?? does not chain without parentheses, because a ?? b on two options is
still an option:
fn main(): void {
a: int? = None
b: int? = Some(1)
println("${a ?? (b ?? 0)}") // explicit grouping
println("${a.or(b).unwrap_or(0)}") // or say it with methods
}Patterns
match destructures both variants and supports guards:
fn describe(size: int?): string {
return match size {
Some(n) if n > 1000 => "large (${n})"
Some(n) if n > 0 => "small (${n})"
Some(_) => "empty"
None => "unknown"
}
}
fn main(): void {
println(describe(Some(5000)))
println(describe(Some(5)))
println(describe(Some(0)))
println(describe(None))
}if Some(x) := option runs a block only when the value is present, and
for Some(x) := option is the loop form. Note the Some(...) pattern: binding
a bare name captures the whole Option, not the payload.
struct Config { name: string? }
fn main(): void {
c := Config { name: Some("release") }
if Some(name) := c.name {
println("configured as ${name}")
}
for Some(name) := c.name {
println("loop saw ${name}")
break
}
}Optional member access ?. short-circuits a whole chain to None:
struct User { name: string, manager: User? }
fn main(): void {
ada := User { name: "ada", manager: None }
grace := User { name: "grace", manager: Some(ada) }
println("${grace.manager?.name ?? "none"}")
println("${grace.manager?.manager?.name ?? "none"}")
println("${ada.manager?.name ?? "none"}")
}Transformation
map applies a function inside the wrapper; filter can turn Some into
None; flat_map is for callbacks that are themselves optional.
fn main(): void {
name: string? = Some(" Ada ")
length := name
.map(n => n.trim())
.filter(n => !n.is_empty())
.map(n => n.len())
println("${length.unwrap_or(0)}")
blank: string? = Some(" ")
trimmed := blank.map(n => n.trim()).filter(n => !n.is_empty())
println("${trimmed.unwrap_or("<blank>")}")
absent: string? = None
println("${absent.map(n => n.len()).unwrap_or(0)}")
}Note the unwrap_or rather than ?? at the end of those chains. Discharging a
combinator’s result with ?? fallback currently fails to lower when the
payload is numeric (ATOLL2004: builtin method to_string has no lowering path); unwrap_or is the reliable form after map / filter / flat_map.
?? is fine on an option that came straight from a binding, a field, or a
function call.
The map / flat_map choice is about the callback’s return type. A callback
returning a plain value wants map; a callback returning an Option wants
flat_map, or you end up holding Option[Option[T]].
struct Project { owner: string? }
fn find_project(id: int): Project? {
if id == 1 { return Some(Project { owner: Some("ada") }) }
return None
}
fn main(): void {
// `p.owner` is already optional, so flat_map keeps one layer.
owner: string? = find_project(1).flat_map(p => p.owner)
println("${owner ?? "unassigned"}")
println("${find_project(2).flat_map(p => p.owner) ?? "unassigned"}")
// `map` here would give string?? — one wrapper too many.
nested := find_project(1).map(p => p.owner)
println("${nested.is_some()}")
}If you already hold a nested option, flat_map(inner => inner) collapses it:
fn main(): void {
nested: int?? = Some(Some(5))
flat: int? = nested.flat_map(inner => inner)
println("${flat.unwrap_or(0)}")
hollow: int?? = Some(None)
println("${hollow.flat_map(inner => inner).unwrap_or(-1)}")
}Option also declares flatten(), but it does not collapse a nested
option. Its signature is fn flatten(self): Option[T], so on an
Option[Option[int]] the type parameter T is already Option[int] and the
result is Option[Option[int]] — the same nesting it started with:
fn main(): void {
nested: int?? = Some(Some(5))
flat: int? = nested.flatten() // ATOLL2002: expected `int?`, found `int??`
println("${flat.unwrap_or(0)}")
}Use flat_map(inner => inner) for the collapse.
Combining
| Method | self is Some |
self is None |
|---|---|---|
or(other) |
keep self |
use other |
or_else(f) |
keep self |
call f() |
and(other) |
use other |
None |
zip(other) |
pair, if other is also Some |
None |
fn from_env(): string? { return None }
fn from_file(): string? { return Some("file-value") }
fn main(): void {
// First source that has a value wins.
resolved := from_env().or(from_file()).unwrap_or("built-in")
println(resolved)
// Lazy: `from_file` runs only when the environment has nothing.
lazy := from_env().or_else(() => from_file()).unwrap_or("built-in")
println(lazy)
}zip is the tool for “both inputs are required”:
struct Point { x: int, y: int }
fn parse_axis(text: string): int? {
if text.is_empty() { return None }
return Some(text.len())
}
fn parse_point(x_text: string, y_text: string): Point? {
return parse_axis(x_text)
.zip(parse_axis(y_text))
.map(pair => Point { x: pair.0, y: pair.1 })
}
fn main(): void {
match parse_point("ab", "cde") {
Some(p) => println("(${p.x}, ${p.y})")
None => println("incomplete input")
}
println("${parse_point("ab", "").is_some()}")
}Conversion
to_result(err) turns absence into a typed failure — the boundary between
“legitimately not present” and “this operation cannot continue”. to_list()
yields zero or one element.
error ValidationError { MissingName, MissingEmail }
struct Draft { name: string?, email: string? }
struct Account { name: string, email: string }
fn finalize(draft: Draft): Account ! ValidationError {
name := draft.name.to_result(ValidationError.MissingName)?
email := draft.email.to_result(ValidationError.MissingEmail)?
return Account { name: name, email: email }
}
fn main(): void {
good := Draft { name: Some("ada"), email: Some("ada@example.com") }
bad := Draft { name: None, email: Some("x@example.com") }
match finalize(good) {
Ok(account) => println("created ${account.name}")
Err(e) => println("rejected")
}
match finalize(bad) {
Ok(account) => println("created ${account.name}")
Err(e) => println("rejected")
}
}Keep the Option when absence is an ordinary, expected outcome; convert to a
Result when the current operation needs the value and has to say why it
stopped.
? also works directly on an Option inside a function that returns an
option — it returns None from the caller on absence:
fn doubled_head(values: []int): int? {
first := values.get(0)?
return Some(first * 2)
}
fn main(): void {
println("${doubled_head([4, 5]) ?? -1}")
println("${doubled_head([]) ?? -1}")
}to_list() is convenient when a stream of optional values should collapse to
the present ones:
fn main(): void {
lookups: []int? = [Some(1), None, Some(3)]
mut present: []int = []
for value in lookups {
for item in value.to_list() {
present.add(item)
}
}
println("${present.len()}")
}Equality, hashing, and text
Option[T] is equatable when T is, and hashable when T is. The variant
participates, so None and Some(v) remain distinct map keys.
fn main(): void {
a: int? = Some(3)
b: int? = Some(3)
c: int? = None
println("${a == b} ${a == c} ${c == None}")
println("${a.to_string()} ${c.to_string()}")
mut counts: Map[int?, string] = Map.new()
counts.put(Some(1), "one")
counts.put(None, "unspecified")
println("${counts.size()} ${counts.get(None) ?? "?"}")
}A worked example
Resolving a setting from three sources in priority order, with a typed failure only when the value is required and nothing supplied it.
error ConfigError { Required { key: string } }
struct Sources {
overrides: Map[string, string]
file: Map[string, string]
defaults: Map[string, string]
}
fn resolve(sources: Sources, key: string): string? {
return sources.overrides.get(key)
.or_else(() => sources.file.get(key))
.or_else(() => sources.defaults.get(key))
}
fn resolve_int(sources: Sources, key: string): int? {
return resolve(sources, key).map(text => text.len())
}
fn require(sources: Sources, key: string): string ! ConfigError {
missing := ConfigError.Required { key: key }
return resolve(sources, key).to_result(missing)?
}
fn main(): void {
mut overrides: Map[string, string] = Map.new()
overrides.put("host", "override-host")
mut file: Map[string, string] = Map.new()
file.put("host", "file-host")
file.put("port", "8080")
mut defaults: Map[string, string] = Map.new()
defaults.put("port", "80")
defaults.put("scheme", "http")
sources := Sources { overrides: overrides, file: file, defaults: defaults }
println("host = ${resolve(sources, "host") ?? "<none>"}")
println("port = ${resolve(sources, "port") ?? "<none>"}")
println("scheme = ${resolve(sources, "scheme") ?? "<none>"}")
println("region = ${resolve(sources, "region") ?? "<none>"}")
println("port width = ${resolve_int(sources, "port") ?? 0}")
match require(sources, "region") {
Ok(value) => println("region ${value}")
Err(e) => println("region is required and unset")
}
}For the ? operator, catch, and how absence interacts with error
propagation, see Option errors.