10 — Option and the railway
Previous: Capabilities
日本語版: 10_option.vibe.md
A value that might not be there has type Option[T]: either Some(v)
or None. It is an ordinary enum — nothing about it is built into the
compiler — but it is common enough that the language has three
shorthands for the thing you always want to do with it, which is stop
early when there is nothing.
The type
fn half(n: Int) -> Option[Int] {
if n % 2 == 0 {
Some(n / 2)
} else {
None
}
}
fn main with Console {
let a = match half(10) {
Some(v) => v,
None => -1
}
let b = match half(3) {
Some(v) => v,
None => -1
}
println("half(10) = \{a}")
println("half(3) = \{b}")
}
half(10) = 5
half(3) = -1
match is always available and always works. The rest of this chapter
is about not writing it four times in a row.
? — unwrap, or return None now
Put ? after an expression of type Option[T] and you get the T. If
it was None, the enclosing function returns None immediately:
fn half(n: Int) -> Option[Int] {
if n % 2 == 0 {
Some(n / 2)
} else {
None
}
}
fn sum_halves(a: Int, b: Int) -> Option[Int] {
let x = half(a)?
let y = half(b)?
Some(x + y)
}
fn main with Console {
println("sum_halves(4, 6) = \{sum_halves(4, 6)}")
println("sum_halves(4, 3) = \{sum_halves(4, 3)}")
}
sum_halves(4, 6) = Some(5)
sum_halves(4, 3) = None
The function has to return an Option itself, which is the honest part
of the deal: ? does not make the absence disappear, it passes it to
your caller.
let* — the same idea for a whole block
let* x = e binds the contents of a Some, and on None the block it
sits in evaluates to None:
fn half(n: Int) -> Option[Int] {
if n % 2 == 0 {
Some(n / 2)
} else {
None
}
}
fn sum_halves(a: Int, b: Int) -> Option[Int] {
let* x = half(a)
let* y = half(b)
Some(x + y)
}
fn main with Console {
println("sum_halves(4, 6) = \{sum_halves(4, 6)}")
println("sum_halves(4, 3) = \{sum_halves(4, 3)}")
}
sum_halves(4, 6) = Some(5)
sum_halves(4, 3) = None
? and let* do the same job here; ? suits one expression in the
middle of a line, let* suits a run of steps that all have to succeed.
guard — bind, or leave
Sometimes you do not want to propagate the None — you want to handle
it and carry on with an unwrapped value. guard binds for the rest of
the scope, and its else must leave:
fn double_or_zero(o: Option[Int]) -> Int {
guard o is Some(v) else {
return 0
}
v * 2
}
fn main with Console {
println("double_or_zero(Some(21)) = \{double_or_zero(Some(21))}")
println("double_or_zero(None) = \{double_or_zero(None)}")
}
double_or_zero(Some(21)) = 42
double_or_zero(None) = 0
Note that v is in scope on the last line, with no nesting — that is
the whole point of guard over match.
The else must actually leave the function, by return or by
throw(...). It has to, because everything after the guard is written
assuming v exists. When the fallback is a value rather than an exit,
use if o is Some(v) { ... } else { ... } instead.
Asking without unwrapping
When you only want to know, is gives you a Bool:
fn half(n: Int) -> Option[Int] {
if n % 2 == 0 {
Some(n / 2)
} else {
None
}
}
fn main with Console {
println("half(10) is Some(_) = \{half(10) is Some(_)}")
println("half(3) is None = \{half(3) is None}")
}
half(10) is Some(_) = true
half(3) is None = true
When absence is not the story
Option says a value is missing. It does not say why, and sometimes
why is the point — a parse failed, a file was malformed. For that, a
function declares with Exception and throws a message, the effect
Effects opened with. Reach for Option when "not
there" is the whole story, and for Exception when the caller deserves a
reason.
Next: Modules and packages.