rectify

Rectify - Railway-oriented programming utilities for Gleam

A port of FsToolkit.ErrorHandling concepts:

Types

Validation applicative - collect multiple errors instead of fail-fast.

Unlike Result which stops at the first Error, Validation accumulates all errors before returning them together.

pub type Validation(a, e) {
  Valid(a)
  Invalid(List(e))
}

Constructors

  • Valid(a)
  • Invalid(List(e))

Values

pub fn apply(
  vf: Validation(fn(a) -> b, e),
  va: Validation(a, e),
) -> Validation(b, e)

Apply a validation containing a function to a validation containing a value.

This is the core operation for applicative functors. It allows you to gradually build up multi-argument functions by applying validations one at a time. Combined with use, this enables elegant sequential validation that still accumulates all errors.

Rationale

While map2 works for two validations, apply lets you handle arbitrary arities by currying: apply the first validation to get a function in a validation, then keep applying remaining validations. This is how map3 through map5 are implemented internally.

Examples

Apply a single-argument function:

apply(valid(fn(x) { x * 2 }), valid(21))
// -> Valid(42)

Errors from both sides accumulate:

apply(invalid("e1"), invalid("e2"))
// -> Invalid(["e1", "e2"])

Build up multi-argument validation incrementally:

// Start with a curried function for creating a User
let user_constructor = valid(fn(name) { fn(age) { User(name, age) } })

// Apply validations one by one, collecting all errors
user_constructor
|> apply(validate_name("Alice"))   // Valid(fn(age) { User("Alice", age) })
|> apply(validate_age(30))         // Valid(User("Alice", 30))
pub fn bind(
  v: Validation(a, e),
  f: fn(a) -> Validation(b, e),
) -> Validation(b, e)

Bind/flatMap for validation (note: doesn’t accumulate errors across binds).

Use this when determining the next validation step depends on the success value of the previous step. Errors from the first step are preserved, but errors do not accumulate with operations in the second step, because the second step is never executed if the first step fails.

Examples

valid(1) |> bind(fn(x) { valid(x * 2) })
// -> Valid(2)
invalid("e") |> bind(fn(x) { valid(x * 2) })
// -> Invalid(["e"])
pub fn errors(v: Validation(a, e)) -> List(e)

Get errors from a validation, or empty list if Valid.

Examples

errors(valid(1))
// -> []
errors(invalid_many(["a", "b"]))
// -> ["a", "b"]
pub fn flatten(
  v: Validation(Validation(a, e), e),
) -> Validation(a, e)

Flatten a nested validation.

Examples

flatten(valid(valid(1)))
// -> Valid(1)
flatten(valid(invalid("e")))
// -> Invalid(["e"])
pub fn invalid(e: e) -> Validation(a, e)

Create a failed validation with a single error.

pub fn invalid_many(es: List(e)) -> Validation(a, e)

Create a failed validation with multiple errors.

pub fn is_invalid(v: Validation(a, e)) -> Bool

Check if validation has errors.

Examples

is_invalid(invalid("e"))
// -> True
is_invalid(valid(1))
// -> False
pub fn is_valid(v: Validation(a, e)) -> Bool

Check if validation is valid.

Examples

is_valid(valid(1))
// -> True
is_valid(invalid("e"))
// -> False
pub fn map(
  validation: Validation(a, e),
  f: fn(a) -> b,
) -> Validation(b, e)

Map a function over a validation.

Examples

valid(5) |> map(fn(n) { n * 2 })
// -> Valid(10)
invalid("error") |> map(fn(n) { n * 2 })
// -> Invalid(["error"])
pub fn map2(
  v1: Validation(a, e),
  v2: Validation(b, e),
  f: fn(a, b) -> c,
) -> Validation(c, e)

Map over two validations, combining their errors if both fail.

Examples

map2(valid(2), valid(3), fn(a, b) { a + b })
// -> Valid(5)
map2(invalid("e1"), invalid("e2"), fn(a, b) { a + b })
// -> Invalid(["e1", "e2"])
pub fn map3(
  v1: Validation(a, e),
  v2: Validation(b, e),
  v3: Validation(c, e),
  combiner: fn(a, b, c) -> d,
) -> Validation(d, e)

Map over three validations.

Similar to map2, but for three validations. Gathers all errors if any of the validations are invalid.

pub fn map4(
  v1: Validation(a, e),
  v2: Validation(b, e),
  v3: Validation(c, e),
  v4: Validation(d, e),
  f: fn(a, b, c, d) -> g,
) -> Validation(g, e)

Map over four validations.

Similar to map2, but for four validations. Gathers all errors if any of the validations are invalid.

pub fn map5(
  v1: Validation(a, e),
  v2: Validation(b, e),
  v3: Validation(c, e),
  v4: Validation(d, e),
  v5: Validation(g, e),
  f: fn(a, b, c, d, g) -> h,
) -> Validation(h, e)

Map over five validations.

Similar to map2, but for five validations. Gathers all errors if any of the validations are invalid.

Why Stop at 5?

Following Miller’s Law (7±2 items in working memory), we cap at 5 for cognitive ergonomics. Need more? Compose validations hierarchically. With just 3 levels of nesting, you can handle 125 fields (5³).

Examples

For larger arities, compose with nested maps:

// A type with 9 fields
type LargeForm {
  LargeForm(
    a: String, b: String, c: String,
    d: String, e: String, f: String,
    g: String, h: String, i: String,
  )
}

// Group into 3 sub-records, validate each group
let group1 = map3(va, vb, vc, SubRecord1)
let group2 = map3(vd, ve, vf, SubRecord2)
let group3 = map3(vg, vh, vi, SubRecord3)

// Combine the groups
map3(group1, group2, group3, fn(g1, g2, g3) {
  LargeForm(g1.a, g1.b, g1.c, g2.a, g2.b, g2.c, g3.a, g3.b, g3.c)
})
pub fn map_errors(
  v: Validation(a, e),
  f: fn(e) -> f,
) -> Validation(a, f)

Transform errors in a validation.

Examples

invalid("e") |> map_errors(fn(e) { "Error: " <> e })
// -> Invalid(["Error: e"])
pub fn of_result(r: Result(a, e)) -> Validation(a, e)

Convert Result to Validation.

Examples

of_result(Ok(42))
// -> Valid(42)
of_result(Error("e"))
// -> Invalid(["e"])
pub fn of_result_list(r: Result(a, List(e))) -> Validation(a, e)

Convert Result with multiple errors to Validation.

Examples

of_result_list(Ok(42))
// -> Valid(42)
of_result_list(Error(["e1", "e2"]))
// -> Invalid(["e1", "e2"])
pub fn to_result(v: Validation(a, e)) -> Result(a, List(e))

Convert validation to Result (all errors as list).

Examples

to_result(valid(42))
// -> Ok(42)
to_result(invalid_many(["a", "b"]))
// -> Error(["a", "b"])
pub fn unwrap(v: Validation(a, e), default: a) -> a

Unwrap a validation, returning the value or a default.

Examples

unwrap(valid(42), 0)
// -> 42
unwrap(invalid("e"), 0)
// -> 0
pub fn unwrap_lazy(v: Validation(a, e), f: fn() -> a) -> a

Unwrap a validation with a lazy default.

Examples

unwrap_lazy(valid(42), fn() { 0 })
// -> 42
unwrap_lazy(invalid("e"), fn() { 0 })
// -> 0
pub fn valid(a: a) -> Validation(a, e)

Create a successful validation.

Search Document