Option & Result reference
Option<T> is how Vilan says “maybe a value” (there is no null), and
Result<T, E> is how it says “this can fail” (there are no exceptions).
Both are plain enums with a large helper-method surface, listed here. For
how they replace null checks and try/catch in practice (including
the !, ?. and ? operators), read Control flow
first.
import std::option::Option::{ self, Some, None };
import std::result::Result::{ self, Ok, Err };
(The { self, … } form imports the type and its variants, so Some(x)
works unqualified.)
Option<T>
enum Option<T> { Some(T), None }
impl Option<type T> {
// predicates
fun is_some(self): bool
fun is_some_and(own self, fn: |T| bool): bool
fun is_none(self): bool
fun is_none_or(own self, fn: |T| bool): bool
// extraction
fun unwrap(own self): T // panics on None
fun expect(own self, lazy message: str): T // panic with your message
fun unwrap_or(own self, lazy fallback: T): T
fun unwrap_or_else(own self, fn: || T): T
// in-place partial move — read/replace the slot through `&mut self`,
// always leaving a valid Option behind
fun take(&mut self): Option<T> // Some(v) -> None here, Some(v) out
fun replace(&mut self, own value: T): Option<T> // value in, old contents out
// transformation
fun map<U>(own self, fn: |T| U): Option<U>
fun map_or<U>(own self, fn: |T| U, fallback: U): U
fun map_or_else<U>(own self, fn: |T| U, fallback: || U): U
fun map_or_default<U: Default>(own self, fn: |T| U): U
fun inspect(own self, fn: |T| void): Self // peek, pass through
fun filter(own self, predicate: |T| bool): Option<T>
// combination
fun and<U>(own self, own b: Option<U>): Option<U>
fun and_then<U>(own self, fn: |T| Option<U>): Option<U>
fun or(own self, b: Option<T>): Option<T>
fun or_else(own self, fn: || Option<T>): Option<T>
fun xor(own self, own b: Option<T>): Option<T>
fun zip<U>(own self, own peer: Option<U>): Option<(T, U)>
// bridging
fun ok_or<E>(own self, err: E): Result<T, E>
fun ok_or_else<E>(own self, err: || E): Result<T, E>
}
impl Option<Option<type T>> { fun flatten(own self): Option<T> }
impl Option<Result<type T, type E>> { fun transpose(own self): Result<Option<T>, E> }
impl Option<type T: Default> { fun unwrap_or_default(own self): T }
impl Option<(type T, type U)> { fun unzip(own self): (Option<T>, Option<U>) }
str.parse_i32(): Option<i32>, str.parse_f64(): Option<f64> and
str.parse_bool(): Option<bool> (all declared here) are the string→value
path, and bool.then_some(value) is the condition→Option one. All three
trim surrounding whitespace and are otherwise strict: the whole remaining
text must be the value. parse_bool accepts "true" and "false" only —
the exact inverse of Display::to_string on a bool, and the set
std::json’s bool reader accepts. "1", "0", "True" and "yes" are
None, because a parse whose accepted set is wider than its writer’s output
silently adopts some other producer’s convention; an app with a legacy store
says so in one line at its own decoder.
That is the whole conversion idiom, and none of it needs hand-writing: out
through Display::to_string, back through str.parse_*, and anything
structured through std::json’s Json / FromJson.
import std::display::Display;
import std::io::print;
import std::option::Option;
fun main() {
let stored = true.to_string();
print(stored.parse_bool().unwrap_or(false));
print("1".parse_bool().is_some());
}
The combinators that hand the payload onward take own self: they move
the value out of the Option, so they must own it (docs/spec/memory.md R3).
For plain data that is invisible — own copies, and opt.unwrap() leaves
opt perfectly readable. For an Option<SomeResource> it is the affine rule
biting: opt.unwrap() moves opt, a later use of opt is a
use-after-move error, and opt is not torn down at all (the payload
you now hold is). The pure predicates — is_some, is_none — keep a
borrowing self and never consume, so they stay free on a resource.
expect is unwrap with your own panic message, and its message is
deferred: it costs nothing on the path that has a value. Both are
[track_caller]: the panic names the line that called them
(panicked at src/main.vl:9:21: expected Some but got None). Reaching a
resource payload is a match, not a guarded unwrap: match opt { Some(let value) => .., None => .. } consumes opt on every path, which
is what R7 requires. if (opt.is_some()) { opt.unwrap() } moves opt on one
path only and is rejected as a conditional move (before this was checked it
compiled and destroyed the payload twice).
The capture owns the payload it took, so it is torn down at its last use
inside the leg unless you move it on — into drop, into a return, into an own
argument, into a struct field. Either way the payload is destroyed exactly
once. A capture taken from a loan (match &opt, or opt is Some(let value))
consumes nothing and owns nothing: opt stays the owner and tears the
payload down itself.
Three combinators are rejected at an Option<SomeResource>, and the rule
is one sentence: each has a path that discards a resource value it was
handed, and a generic body cannot destroy a T. opt.or(other) throws
other away when opt is Some; opt.xor(other) throws both away when both
are Some; opt.unwrap_or(fallback) throws the fallback away when opt is
Some. Each names the discarded value in its diagnostic. The spellings that
produce the alternative instead of taking one in — or_else,
unwrap_or_else — work, because nothing is ever discarded. Everything else on
this surface is fine at a resource, including inspect and ==, which loan
rather than consume.
take and replace mutate the Option in place through &mut self: take
swaps None in and hands the old contents back, replace swaps a new value in
and hands the old back. Both leave a valid Option behind, which is what makes
them the sanctioned way to move a value out of a place. For a resource this
is the partial move that works through a loan, or out of an aggregate with a
Drop inside (self.slot.take()), and match opt.take() { Some(let c) => drop(c), None => {} } is the conditional-teardown idiom.
Result<T, E>
enum Result<T, E> { Ok(T), Err(E) }
impl Result<type T, type E> {
// predicates
fun is_ok(self): bool
fun is_ok_and(self, fn: |T| bool): bool
fun is_err(self): bool
fun is_err_and(self, fn: |E| bool): bool
// extraction
fun unwrap(self): T // panics on Err
fun unwrap_err(self): E // panics on Ok
fun unwrap_or(self, lazy fallback: T): T
fun unwrap_or_else(self, fn: |E| T): T
fun expect(self, lazy message: str): T // panic with your message
fun expect_err(self, lazy message: str): E
// transformation
fun map<U>(self, fn: |T| U): Result<U, E>
fun map_err<F>(self, fn: |E| F): Result<T, F>
fun map_or<U>(self, fn: |T| U, fallback: U): U
fun map_or_else<U>(self, fn: |T| U, fallback: |E| U): U
fun inspect(self, fn: |T| void): Self
fun inspect_err(self, fn: |E| void): Self
// combination
fun and<U>(self, b: Result<U, E>): Result<U, E>
fun and_then<U>(self, fn: |T| Result<U, E>): Result<U, E>
fun or<F>(self, b: Result<T, F>): Result<T, F>
fun or_else<F>(self, fn: |E| Result<T, F>): Result<T, F>
// bridging
fun ok(self): Option<T>
fun err(self): Option<E>
}
impl Result<type T: Default, type E> { fun unwrap_or_default(self): T }
impl Result<Option<type T>, type E> { fun transpose(self): Option<Result<T, E>> }
Idioms
import std::option::Option::{ self, Some, None };
import std::result::Result::{ self, Ok, Err };
fun parse_port(text: str): Result<i32, str> {
text.parse_i32()
.ok_or(i"not a number: {text}")
.and_then(|port| {
if port > 0 && port < 65536 {
Ok(port)
} else {
Err(i"out of range: {port}")
}
})
}
fun main() {
print(parse_port("8080").unwrap_or(0));
match parse_port("http") {
Ok(let port) => print(port),
Err(let reason) => print(reason),
}
}
- Prefer
!(propagate) andunwrap_or*overunwrap:unwrapis for invariants, and it panics. unwrap_or’s fallback andexpect’s message arelazyon all five ofOption::expect,Option::unwrap_or,Result::expect,Result::unwrap_orandResult::expect_err: the expression you write in that position is evaluated only on the path that needs it, at most once. Soopt.unwrap_or(expensive())does not runexpensive()whenoptisSome, andres.expect(i"no row for {key}")builds no message when the row is there — nor doesres.expect_err(..)on theErrpath, which is the one it does not panic on.unwrap_or_elseremains the explicit spelling — a closure, and on aResultthe one that sees the error — and it is still the resource-clean one, because it PRODUCES the fallback instead of discarding it.- Application errors belong in
Result’sE; only unreachable states panic. matchwithSome(let x)/Ok(let x)patterns is always available when the method chain gets clever. Clarity beats cleverness.