on
gleam add on@1
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.
Overview
All package functions adhere to the same pattern as:
// 'on' package
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:
// 'on' consumer
use ok_payload <- on.error_ok(
some_result,
on_error: fn (error_payload) { /* map error_payload to desired return value here */ },
)
// ...keep working with 'ok_payload' down here
Symmetrically, for example,
on.ok_error
allows the Error
variant to
correspond to the happy path instead; per the
consumer:
// 'on' consumer
use error_payload <- on.ok_error(
some_result,
on_ok: fn (ok_payload) { /* map ok_payload to desired return value here */ },
)
// ...keep working with 'error_payload' down here
The complete list of similar two-variant guards provided by the package is:
// 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
Note that 0-ary variants expect values
instead of values by default, following the convention
of the Gleam stdlib. As in the standard
library as well, apply the lazy_
prefix to access lazy
evaluation versions:
on.lazy_none_some // takes 0-ary callback instead of value for `on_none`
on.lazy_true_false // takes 0-ary callback instead of value for `on_true`
on.lazy_false_true // takes 0-ary callback instead of value for `on_false`
on.lazy_empty_nonempty // takes 0-ary callback instead of value for `on_empty`
One-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' package
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.:
// 'on' consumer
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' package
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.:
// 'on' consumer
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.continue
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”. An application might be a case where
some side-effect such as printing to I/O is desired for only one
half of a boolean value.)
Ternary guards for List(a) values
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:
on.empty_singleton_gt1
on.empty_gt1_singleton
on.singleton_gt1_empty
on.lazy_empty_singleton_gt1
on.lazy_empty_gt1_singleton
For example, on.lazy_empty_singleton_gt1
has the
following implementation and usage:
// 'on' package
pub fn lazy_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)
}
}
// 'on' consumer
use first, second, rest <- on.lazy_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/Continue mechanism
The package also offers a one-size-fits-all guard named
on.continue
that consumes a value of type Return(a, b)
:
// 'on' package
pub type Return(a, b) {
Return(a)
Continue(b)
}
Specifically, given a Return(a, b)
value, on.continue
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 Continue(b)
:
// 'on' package
pub fn continue(
r: Return(a, b),
on_continue f: fn(b) -> a,
) -> a {
case r {
Return(a) -> a
Continue(b) -> f(b)
}
}
This allows some many-valued variant to be sorted into
Return
and Continue
buckets; the restriction being that
all Return
buckets contain the same type a
, that all
Continue
buckets contain the same type b
, and that
code below the on.continue
needs to resolve
to a value of type a
, as well:
// 'on' consumer
import on.{Continue, Return}
use b <- on.continue(
case some_5_variant_thing() {
Variant1(v1) -> Return( /* construct value of type a from v1 */ )
Variant2(v2) -> Return( /* construct value of type a from v2 */ )
Variant3(v3) -> Return( /* construct value of type a from v3 */ )
Variant4(v4) -> Continue( /* construct value of type b from v4 */ )
Variant5(v5) -> Continue( /* construct value of type b from v5 */ )
}
)
// ...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 with a different variety of guards.
Additional Examples
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.lazy_empty_nonempty(
string.split(contents, "\n"),
on_empty: fn() { io.println("empty contents") },
)
use <- on.lazy_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) { 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"),
on_true: #(string.drop_end(s, 3), Some(REM)),
)
use <- on.true_false(
string.ends_with(s, "em"),
on_true: #(string.drop_end(s, 2), Some(EM)),
)
use <- on.true_false(
string.ends_with(s, "px"),
on_true: #(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))
}