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 allows 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 allows 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* — leave the block, not the function

let* x = e binds the contents of a Some, and on None the block it sits in evaluates to None. The function goes on after that block:

fn half(n: Int) -> Option[Int] {
  if n % 2 == 0 {
    Some(n / 2)
  } else {
    None
  }
}

fn halves_or_zero(a: Int, b: Int) -> Int {
  let total = {
    let* x = half(a)
    let* y = half(b)
    Some(x + y)
  }
  match total {
    Some(t) => t,
    None => 0
  }
}

fn main allows Console {
  println("halves_or_zero(4, 6) = \{halves_or_zero(4, 6)}")
  println("halves_or_zero(4, 3) = \{halves_or_zero(4, 3)}")
}
halves_or_zero(4, 6) = 5
halves_or_zero(4, 3) = 0

That is the whole difference between the two, and the reason both exist: ? leaves the enclosing function, let* leaves the enclosing block. halves_or_zero returns an Int, so ? could not appear in it at all. Directly in a function body's own block the two leave the same place, and there ? is the one spelling: vibe check warns on a let* written at a function body's top level and names the ? rewrite.

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 allows 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 allows 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.