on

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.

Overview

All package functions adhere to the pattern:

// 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 0-ary callback for `on_error`
on.eager_ok_error         // takes a value instead of a 0-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 0-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 0-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 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 than to work with ternary guards, though ternary guards are kept available. See below.

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 some many-valued variant to be sorted into Return and Stay buckets; the restriction being that all Return buckets contain a payload of same type a, that all Stay buckets contain a payload of same type b, and that the code below the on.stay resolves to a value of type a, as well:

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, older and more established. See table below.

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

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.eager_true_false(
    string.ends_with(s, "rem"),
    on_true: #(string.drop_end(s, 3), Some(REM)),
  )

  use <- on.eager_true_false(
    string.ends_with(s, "em"),
    on_true: #(string.drop_end(s, 2), Some(EM)),
  )

  use <- on.eager_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))
}