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.
//// `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)])
}//// `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.
import gleam/dict.{type Dict}
import gleam/int
import gleam/io
import gleam/list
import gleam/result.{try as bea_result_try}
pub type User {
User(name: String, age: Int)
}
pub type Problem {
Missing(field: String)
NotANumber(text: String)
TooYoung(age: Int)
}
/// A boolean guard: the condition must hold.
fn check_age(age: Int) -> Result(Int, Problem) {
case age >= 18 {
True -> Ok(age)
False -> Error(TooYoung(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 fn sign_up(form: Dict(String, String)) -> Result(User, Problem) {
case dict.get(form, "name") {
Ok(name) ->
case dict.get(form, "age") {
Ok(text) ->
case int.parse(text) {
Ok(age) -> {
use age <- bea_result_try(check_age(age))
Ok(User(name: name, age: age))
}
Error(_) -> Error(NotANumber(text))
}
_ -> Error(Missing("age"))
}
_ -> Error(Missing("name"))
}
}
/// `else` can bind what didn't match. Here the error is turned into text.
pub fn greeting(form: Dict(String, String)) -> String {
case sign_up(form) {
Ok(User(name: name, ..)) -> "welcome, " <> name
Error(problem) -> explain(problem)
}
}
fn 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 fn first_adult(users: List(User)) -> String {
case list.filter(users, fn(u) { u.age >= 18 }) {
[adult, ..] -> adult.name
_ -> "nobody"
}
}
/// `use` still works: it suits code that wraps the rest of the function,
/// like this logging (on stderr, with `echo`).
fn logged(label: String, next: fn() -> a) -> a {
io.println_error("-> " <> label)
let result = next()
io.println_error("<- " <> label)
result
}
pub fn 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)])
}-> sign up
src/else_guards.bea:60
"welcome, Ada"
src/else_guards.bea:61
"two is not a number"
src/else_guards.bea:62
"come back in 3 years"
src/else_guards.bea:63
"name is missing"
src/else_guards.bea:64
"Ada"
<- sign upThis 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