rectify
Rectify - Railway-oriented programming utilities for Gleam
A port of FsToolkit.ErrorHandling concepts:
- Validation: Applicative functor for collecting multiple errors
- Result/Option utilities for composing fallible operations
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