Chapter 7: Records, guards and deriving

Else guards

  • else
  • pattern guards
  • ?
  • use

A guard is an assignment that must match, followed by what to return when it doesn't: Ok(name) = dict.get(form, "name") else Error(Missing("name")). When the value doesn't match, the function returns what follows else, and the rest of the block never runs.

Guards come in a few shapes. A boolean guard is just a condition: age >= 18 else Error(TooYoung(age)). A pattern guard takes any pattern: Ok and Error, your own types, lists like [adult | _]. With ?, the pattern is for the Ok value, and else problem -> … binds the error, so you can turn it into something else. Any guard can bind what didn't match that way.

Guards are a way to leave a function early without callbacks, next to return and ?: each check reads top to bottom and the code stays flat. The Gleam tab shows the nested case expressions they become. use still works, and it is the right tool for code that wraps the rest of a function, like logged here.

Open in playground
//// `else` guards: an assignment that has to match, and what to return when
//// it doesn't. Each check reads top to bottom, and the rest of the
//// function stays flat.

pub type User { name: String, age: Int }

pub type Problem {
  Missing(field: String)
  NotANumber(text: String)
  TooYoung(age: Int)
}

/// A boolean guard: the condition must hold.
def check_age(age: Int) -> Result(Int, Problem) {
  age >= 18 else Error(TooYoung(age))
  Ok(age)
}

/// A pattern guard: `Ok(name)` must match, or the function returns.
/// With `?`, the pattern is for the `Ok` value and `else` can bind the error.
pub def sign_up(form: Dict(String, String)) -> Result(User, Problem) {
  Ok(name) = dict.get(form, "name") else Error(Missing("name"))
  Ok(text) = dict.get(form, "age") else Error(Missing("age"))
  age = int.parse(text)? else _ -> Error(NotANumber(text))
  age = check_age(age)?
  Ok(User(name:, age:))
}

/// `else` can bind what didn't match. Here the error is turned into text.
pub def greeting(form: Dict(String, String)) -> String {
  User(name: | _) = sign_up(form)? else problem -> explain(problem)
  "welcome, #{name}"
}

def explain(problem: Problem) -> String {
  case problem {
    Missing(field) -> "#{field} is missing"
    NotANumber(text) -> "#{text} is not a number"
    TooYoung(age) -> "come back in #{int.to_string(18 - age)} years"
  }
}

/// Any pattern works, lists included.
pub def first_adult(users: List(User)) -> String {
  [adult | _] = list.filter(users, fn(u) { u.age >= 18 }) else "nobody"
  adult.name
}

/// `use` still works: it suits code that wraps the rest of the function,
/// like this logging (on stderr, with `echo`).
def logged(label: String, next: fn() -> a) -> a {
  io.println_error("-> " <> label)
  result = next()
  io.println_error("<- " <> label)
  result
}

pub def main() {
  use <- logged("sign up")
  echo greeting(dict.from_list([("name", "Ada"), ("age", "36")]))
  echo greeting(dict.from_list([("name", "Bea"), ("age", "two")]))
  echo greeting(dict.from_list([("name", "Kit"), ("age", "15")]))
  echo greeting(dict.from_list([("age", "40")]))
  echo first_adult([User("Kit", 15), User("Ada", 36)])
}

This lesson's code is a module of a small Bea project, compiled and run by bea for this page: content/features/src/else_guards.bea