Skip to content

Option

Model absence with Option[T], and transform, combine, and discharge it without panics.

Updated View as Markdown

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("[email protected]") }
    bad := Draft { name: None, email: Some("[email protected]") }

    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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close