on

logo

Package Version Hex Docs

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))
}
Search Document