on

Gleam On Package Logo

Package VersionHex Docs

gleam add on@3

The ‘on’ package consists of a collection of guards that can be paired with Gleam's <- use syntax. The package replicates some functions from the Gleam stdlib under a uniform naming scheme.

API digest.

Overview

All package functions adhere to a similar pattern as:

// on.gleam
pub fn error_ok(
result: Result(a, b),
on_error f1: fn(b) -> c,
on_ok f2: fn(a) -> c,
) -> c {
case result {
Error(b) -> f1(b)
Ok(a) -> f2(a)
}
}

With corresponding usage:

import on
use ok_payload <- on.error_ok(
some_result,
on_error: fn(error_payload) {
// map error_payload to return value
},
)
// map ok_payload to return value

E.g., symmetrically, on.ok_error allows the Error variant to correspond to the happy path instead:

import on
use error_payload <- on.ok_error(
some_result,
on_ok: fn(ok_payload) {
// map ok_payload to return value
},
)
// map error_payload to return value

The package contains similar two-variant guards for Bool, Option and List:

// Result
on.error_ok
on.ok_error
// Option
on.none_some
on.some_none
// Bool
on.true_false
on.false_true
// List
on.empty_nonempty
on.nonempty_empty

E.g., usage of on.empty_nonempty:

import on
use first, rest <- on.empty_nonempty(
some_list(),
on_empty: fn() {
// choose return value for empty list
}
)
// ...work down here with first: a and rest: List(a), in the
// case where the list is nonempty

Values can be provided instead of callbacks by using the eager_ prefix. Specifically, the call names are:

on.eager_error_ok // takes a value instead of a 1-ary callback for `on_error`
on.eager_ok_error // takes a value instead of a 1-ary callback for `on_ok`
on.eager_none_some // takes a value instead of a 0-ary callback for `on_none`
on.eager_some_none // takes a value instead of a 1-ary callback for `on_some`
on.eager_true_false // takes a value instead of a 0-ary callback for `on_true`
on.eager_false_true // takes a value instead of a 0-ary callback for `on_false`
on.eager_empty_nonempty // takes a value instead of a 0-ary callback for `on_empty`
on.eager_nonempty_empty // takes a value instead of a 2-ary callback for `on_nonempty`

E.g.:

import on
use ok_payload <- on.eager_error_ok(some_result, None)
// keep working with ok_payload down here while the
// Error case has been escaped with a None return value;
// this scope must return an Option(a) to match the on_error return type

Single-variant shorthands

Specialized API functions have names that refer to only one variant when the simple identity-like mapping (e.g. mapping None variant of an Option(a) to the None variant of an Option(b)) should be used for the second (elided) variant.

For example, on.some only expects one callback—the second callback defaults to mapping a None: Option(a) to a None: Option(b):

// on.gleam
pub fn some(
option: Option(a),
on_some f2: fn(a) -> Option(b),
) -> Option(b) {
case option {
None -> None
Some(a) -> f2(a)
}
}

E.g.:

import on
use x <- on.some(option_value)
// work with payload x down here, in case option_value == Some(x);
// otherwise code has already returned None

Similarly, on.ok only expects a callback for the Ok payload:

// on.gleam
pub fn ok(
result: Result(a, b),
on_ok f2: fn(a) -> Result(c, b),
) -> Result(c, b) {
case result {
Error(b) -> Error(b)
Ok(a) -> f2(a)
}
}

E.g.:

import on
use x <- on.ok(result_value)
// work with payload x down here, in case result_value == Ok(x);
// otherwise code has already returned Error(b)

One can note that on.ok is isomorphic to result.try from the standard library.

Etc. The list of all 1-callback API functions, excluding on.stay discussed below, is:

on.ok // maps Error(b) to Error(b)
on.error // maps Ok(a) to Ok(a)
on.some // maps None to None
on.none // maps Some(a) to Some(a)
on.true // maps False to False
on.false // maps True to True
on.empty // maps [first, ..rest] to [first, ..rest]
on.nonempty // maps [] to []

Note that on.true and on.false are expected to get less use as it is somewhat unusual to want to early-return only "one half of a boolean". (A possible use case might be that a side-effect such as printing to I/O is desired for only one half of a boolean value before returning.)

Ternary guards for List(a) values

Warning. In the experience of the package author it is less of a headache to use the generic on.stay pattern described below than to work with ternary guards, though ternary guards are included for completeness.

At the other end of the spectrum 'on' provides API functions that take three callbacks for List(a) values, specifically to distinguish between the cases where a list has 0, 1, or greater than 1 values, with the second and last being named as singleton, gt1 respectively in function names. Including all of the eager_ variants this API consists of:

on.empty_singleton_gt1
on.empty_gt1_singleton
on.singleton_gt1_empty
on.eager_empty_singleton_gt1
on.empty_eager_singleton_gt1
on.eager_empty_eager_singleton_gt1
on.eager_empty_gt1_singleton
on.empty_eager_gt1_singleton
on.eager_empty_eager_gt1_singleton
on.eager_singleton_gt1_empty
on.singleton_eager_gt1_empty
on.eager_singleton_eager_gt1_empty

For example, on.empty_singleton_gt1 has the following implementation and usage:

// on.gleam
pub fn empty_singleton_gt1(
list: List(a),
on_empty f1: fn() -> c,
on_singleton f2: fn(a) -> c,
on_gt1 f3: fn(a, a, List(a)) -> c,
) -> c {
case list {
[] -> f1()
[first] -> f2(first)
[first, second, ..rest] -> f3(first, second, rest)
}
}
import on
use first, second, rest <- on.empty_singleton_gt1(
some_list : List(a),
fn() { ... },
fn(some_element: a) { ... },
)
// keep working with first: a, second: a, and rest: List(a)
// down here

Generic Return/Stay mechanism

The package also offers a one-size-fits-all guard named on.stay that consumes a value of type Return(a, b), defined as:

// on.gleam
pub type Return(a, b) {
Return(a)
Stay(b)
}

Specifically, given a Return(a, b) value, on.stay returns the a-payload if the value has the form Return(a) or else applies a given callback of type f(b) -> a to the b-payload if the value has the form Stay(b):

// on.gleam
pub fn stay(
r: Return(a, b),
on_stay f: fn(b) -> a,
) -> a {
case r {
Return(a) -> a
Stay(b) -> f(b)
}
}

This allows arbitrary logic to be sorted into Return and Stay buckets, the restriction being that all Return buckets must contain payloads of the same type and that likewise all Stay buckets must contain payloads of some other same type, while the code below on.stay must resolve to a value of the type contained in the Return buckets. E.g.:

import on
use b <- on.stay(
case some_5_variant_thing() {
Variant1(v1) -> on.Return(
// construct value of type a from v1 here
)
Variant2(v2) -> on.Return(
// construct value of type a from v2 here
)
Variant3(v3) -> on.Return(
// construct value of type a from v3 here
)
Variant4(v4) -> on.Stay(
// construct value of type b from v4 here
)
Variant5(v5) -> on.Stay(
// construct value of type b from v5 here
)
}
)
// ...down here, code that evaluates to type a with access to value
// b; this code only executes if some_5_variant_thing() is Variant4
// or Variant5

See also

The given package, offering similar functionality with a slightly different API.

Table: Comparison between on, stdlib and given

on stdlib given
=============================================================================
// 1-callback guards:
on.ok result.try --
on.error result.try_recover --
on.some option.then --
on.none -- --
on.true -- --
on.false -- --
on.empty -- --
on.nonempty -- --
on.stay -- --
// 2-callback guards lazy versions:
on.error_ok -- given.ok
on.ok_error -- given.error
on.none_some -- given.some
on.some_none -- given.none
on.true_false bool.lazy_guard given.that
on.false_true -- given.not
on.empty_nonempty -- given.non_empty
on.nonempty_empty -- given.empty
-- -- given.any // (for List(Bool) value)
-- -- given.all // (for List(Bool) value)
-- -- given.any_not // (for List(Bool) value)
-- -- given.all_not // (for List(Bool) value)
-- -- given.when // (for fn() -> Bool value)
-- -- given.when_not // (for fn() -> Bool value)
-- -- given.any_ok // (for List(Result) value)
-- -- given.all_ok // (for List(Result) value)
-- -- given.any_error // (for List(Result) value)
-- -- given.all_error // (for List(Result) value)
-- -- given.any_some // (for List(Option) value)
-- -- given.all_some // (for List(Option) value)
-- -- given.any_none // (for List(Option) value)
-- -- given.all_none // (for List(Option) value)
// 2-callback guards eager versions:
on.eager_error_ok -- --
on.eager_ok_error -- --
on.eager_none_some -- --
on.eager_some_none -- --
on.eager_true_false bool.guard --
on.eager_false_true -- --
on.eager_empty_nonempty -- --
on.eager_nonempty_empty -- --
// 3-callback guards lazy versions:
on.empty_singleton_gt1 -- --
on.empty_gt1_singleton -- --
on.singleton_gt1_empty -- --
// 3-callback guards singly & doubly eager_:
on.eager_empty_singleton_gt1 -- --
on.empty_eager_singleton_gt1 -- --
on.eager_empty_gt1_singleton -- --
on.empty_eager_gt1_singleton -- --
on.eager_singleton_gt1_empty -- --
on.singleton_eager_gt1_empty -- --
on.eager_empty_eager_singleton_gt1 -- --
on.eager_empty_eager_gt1_singleton -- --
on.eager_singleton_eager_gt1_empty -- --

Author's milquetoast namespace advice

Users may find that maximum readability is achieved by coupling on. calls to the use <- syntax while otherwise reverting to stdlib calls when an stdlib equivalent is available. (E.g., prefer |> result.try over |> on.ok.)

Additional Examples

import on
fn clamp(v: Float, lower: Float, upper: Float) -> Float {
use <- on.eager_true_false(v <. lower, lower)
use <- on.eager_true_false(v >. upper, upper)
v
}
import gleam/io
import gleam/string
import on
import simplifile
pub fn main() -> Nil {
use contents <- on.error_ok(
simplifile.read("./sample.txt"),
on_error: fn(e) { io.println("simplifile.read error: " <> string.inspect(e)) }
)
use first, rest <- on.empty_nonempty(
string.split(contents, "\n"),
on_empty: fn() { io.println("empty contents") },
)
use <- on.false_true(
string.trim(first) == "<!DOCTYPE html>",
on_false: fn() { io.println("expecting vanilla DOCTYPE in first line") },
)
use parse_tree <- on.error_ok(
parse_html(rest),
on_error: fn(e) { io.println("html parse error: " <> string.inspect(e)) },
)
// ...
}
import gleam/float
import gleam/int
import gleam/option.{type Option, None, Some}
import gleam/string
import on
type CSSUnit {
PX
REM
EM
}
fn extract_css_unit(s: String) -> #(String, Option(CSSUnit)) {
use <- on.true_false(
string.ends_with(s, "rem"),
fn() { #(string.drop_end(s, 3), Some(REM)) },
)
use <- on.true_false(
string.ends_with(s, "em"),
fn() { #(string.drop_end(s, 2), Some(EM)) },
)
use <- on.true_false(
string.ends_with(s, "px"),
fn() { #(string.drop_end(s, 2), Some(PX)) },
)
#(s, None)
}
fn parse_to_float(s: String) -> Result(Float, Nil) {
case float.parse(s), int.parse(s) {
Ok(number), _ -> Ok(number)
_, Ok(number) -> Ok(int.to_float(number))
_, _ -> Error(Nil)
}
}
pub fn parse_number_and_optional_css_unit(
s: String,
) -> Result(#(Float, Option(CSSUnit)), Nil) {
let #(before_unit, unit) = extract_css_unit(s)
use number <- on.ok(parse_to_float(before_unit)) // on.ok === result.try
Ok(#(number, unit))
}