Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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) and unwrap_or* over unwrap: unwrap is for invariants, and it panics.
  • unwrap_or’s fallback and expect’s message are lazy on all five of Option::expect, Option::unwrap_or, Result::expect, Result::unwrap_or and Result::expect_err: the expression you write in that position is evaluated only on the path that needs it, at most once. So opt.unwrap_or(expensive()) does not run expensive() when opt is Some, and res.expect(i"no row for {key}") builds no message when the row is there — nor does res.expect_err(..) on the Err path, which is the one it does not panic on. unwrap_or_else remains the explicit spelling — a closure, and on a Result the 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’s E; only unreachable states panic.
  • match with Some(let x) / Ok(let x) patterns is always available when the method chain gets clever. Clarity beats cleverness.