Packages

A Gleam package to help you thread state through a series of steps.

Current section

Files

Jump to
eval src eval.gleam
Raw

src/eval.gleam

////
import gleam/bool
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/pair
// -----------------------------------------------------------------------------
/// A `Eval` represents a computation to be run given some context. That "to be
/// run" part turns out to be quite powerful. By combining `Eval`s using some
/// of the functions in this module, we can build up a computation that has
/// access to a sort of mutable state that is updated as the computations are
/// run.
///
/// There are three type parameters here, not just two, because an `Eval` also
/// represents a computation that can fail. In many ways, an `Eval` is just a
/// superpowered `Result`!
///
pub opaque type Eval(a, e, ctx) {
Eval(run: fn(ctx) -> #(ctx, Result(a, e)))
}
/// Given an `Eval`, actuall perform the computation by also providing the context
/// that the computation is running in.
///
pub fn run(eval: Eval(a, e, ctx), with context: ctx) -> Result(a, e) {
eval.run(context) |> pair.second
}
/// Step through an `Eval` and get back both it's result and the context it
/// produced. This is especially useful if you want to run some computation,
/// do some other Gleam bits, and then continue with the computation by passing
/// the produced context to `run` or `step` again.
///
pub fn step(eval: Eval(a, e, ctx), ctx: ctx) -> #(ctx, Result(a, e)) {
eval.run(ctx)
}
// -----------------------------------------------------------------------------
// CONSTRUCTORS
// -----------------------------------------------------------------------------
/// Construct an `Eval` that always succeeds with the given value, regardless of
/// context.
///
/// 📝 Note: you might find this called `pure` or `return` in some other languages
/// like Haskell or PureScript.
///
pub fn return(value: a) -> Eval(a, e, ctx) {
use ctx <- Eval
#(ctx, Ok(value))
}
/// Construct an `Eval` that always fails with the given error, regardless of
/// context. Often used in combination with `then` to run some `Eval` and then
/// potentially fail based on the result of that computation.
///
/// ```gleam
/// eval(expr) |> then(fn (y) {
/// case y == 0.0 {
/// True ->
/// throw(DivisionByZero)
///
/// False ->
/// succeed(y)
/// }
/// })
/// ```
///
pub fn throw(error: e) -> Eval(a, e, ctx) {
use ctx <- Eval
#(ctx, Error(error))
}
/// Construct an `Eval` from a function that takes some context and returns a pair
/// of a new context and some `Result` value. This is provided as a fallback if
/// none of the functions here or in `eval/context` are getting you where you need
/// to go: generally you should avoid using this in favour of _combining_ the
/// other functions in this module!
///
pub fn from(eval: fn(ctx) -> #(ctx, Result(a, e))) -> Eval(a, e, ctx) {
Eval(eval)
}
/// Construct an `Eval` from an optional value and an error to throw if that value
/// is `None`. This is useful for situations where you have some function or value
/// that returns an `Option` but is not dependent on the context.
///
pub fn from_option(value: Option(a), error: e) -> Eval(a, e, ctx) {
case value {
Some(a) -> return(a)
None -> throw(error)
}
}
/// Construct an `Eval` from a result. This is useful for situations where you have
/// some function or value that returns a `Result` but is not dependent on the
/// context.
///
pub fn from_result(value: Result(a, e)) -> Eval(a, e, ctx) {
case value {
Ok(a) -> return(a)
Error(e) -> throw(e)
}
}
// -----------------------------------------------------------------------------
// MANIPULATIONS
// -----------------------------------------------------------------------------
/// Transform the value produced by an `Eval` using the given function.
///
/// 📝 Note: you might find this called `fmap` or `<$>` in some other languages
/// like Haskell or PureScript. In this context, the `Eval` type would be known
/// as a _functor_.
///
pub fn map(eval: Eval(a, e, ctx), by f: fn(a) -> b) -> Eval(b, e, ctx) {
use ctx <- Eval
let #(ctx, result) = eval.run(ctx)
case result {
Ok(a) -> #(ctx, Ok(f(a)))
Error(e) -> #(ctx, Error(e))
}
}
///
///
/// 📝 Note: you might find this called `liftA2` or `liftM2` in some other
/// languages like Haskell or PureScript.
///
pub fn map2(
eval_a: Eval(a, e, ctx),
eval_b: Eval(b, e, ctx),
by f: fn(a, b) -> c,
) -> Eval(c, e, ctx) {
use ctx <- Eval
let #(ctx, res) = eval_a.run(ctx)
case res {
Ok(a) -> {
let #(ctx, res) = eval_b.run(ctx)
case res {
Ok(b) -> #(ctx, Ok(f(a, b)))
Error(e) -> #(ctx, Error(e))
}
}
Error(e) -> #(ctx, Error(e))
}
}
/// Just like `map` but for error-producing steps instead. Transforms the error
/// produced by some `Eval` step using the given function.
///
pub fn map_error(eval: Eval(a, e, ctx), by f: fn(e) -> x) -> Eval(a, x, ctx) {
use ctx <- Eval
let #(ctx, result) = eval.run(ctx)
case result {
Ok(a) -> #(ctx, Ok(a))
Error(e) -> #(ctx, Error(f(e)))
}
}
/// Run an `Eval` step but then replace its result with some other fixed value.
/// Often used in tandem with effectful steps that often _do_ something but don't
/// produce any meaninful value (and so are usually `Eval(Nil, e, ctx)`).
///
pub fn replace(eval: Eval(a, e, ctx), with replacement: b) -> Eval(b, e, ctx) {
use ctx <- Eval
let #(ctx, result) = eval.run(ctx)
case result {
Ok(_) -> #(ctx, Ok(replacement))
Error(e) -> #(ctx, Error(e))
}
}
/// Just like `replace` but for error-producing steps instead. Replaces the error
/// thrown by some `Eval` step with another, fixed, value.
///
pub fn replace_error(
eval: Eval(a, e, ctx),
with replacement: x,
) -> Eval(a, x, ctx) {
use ctx <- Eval
let #(ctx, result) = eval.run(ctx)
case result {
Ok(a) -> #(ctx, Ok(a))
Error(_) -> #(ctx, Error(replacement))
}
}
// -----------------------------------------------------------------------------
// COMBINATORS
// -----------------------------------------------------------------------------
/// Run an `Eval` and then apply a function that returns another `Eval` to the
/// result. This can be useful for chaining together multiple `Eval`s.
///
/// 📝 Note: you might find this called `bind`, `>>=`, `flatMap`, or `andThen` in
/// some other languages like Haskell, Elm, or PureScript. In this context, the
/// `Eval` type would be known as a _monad_.
///
pub fn then(
eval: Eval(a, e, ctx),
do f: fn(a) -> Eval(b, e, ctx),
) -> Eval(b, e, ctx) {
use ctx <- Eval
let #(ctx, result) = eval.run(ctx)
case result {
Ok(a) -> step(f(a), ctx)
Error(e) -> #(ctx, Error(e))
}
}
/// Run an `Eval` and then apply a function that returns another `Eval` to the
/// result. This can be useful for chaining together multiple `Eval`s. This is
/// the same as [`then`](#then) but you might find the `try` naming nicer to use
/// with Gleam's `use` notation.
///
/// 📝 Note: you might find this called `bind`, `>>=`, `flatMap`, or `andThen` in
/// some other languages like Haskell, Elm, or PureScript. In this context, the
/// `Eval` type would be known as a _monad_.
///
pub fn try(
eval: Eval(a, e, ctx),
then f: fn(a) -> Eval(b, e, ctx),
) -> Eval(b, e, ctx) {
use ctx <- Eval
let #(ctx, result) = eval.run(ctx)
case result {
Ok(a) -> step(f(a), ctx)
Error(e) -> #(ctx, Error(e))
}
}
///
///
pub fn guard(
when requirement: Bool,
return consequence: e,
otherwise do: fn() -> Eval(a, e, ctx),
) -> Eval(a, e, ctx) {
bool.guard(requirement, throw(consequence), do)
}
/// Run a list of `Eval`s in sequence and then combine their results into a list.
/// If any of the `Eval`s fail, the whole sequence fails.
///
/// 📝 Note: you might find this called `sequence` in some other languages like
/// Haskell or PureScript.
///
///
pub fn all(evals: List(Eval(a, e, ctx))) -> Eval(List(a), e, ctx) {
let prepend = fn(list, a) { [a, ..list] }
let callback = fn(a, list) { map2(a, list, prepend) }
list.fold(evals, return([]), callback)
|> map(list.reverse)
}
/// Run an `Eval` and then attempt to recover from an error by applying a function
/// that takes the error value and returns another `Eval`.
///
pub fn attempt(
eval: Eval(a, e, ctx),
catch f: fn(ctx, e) -> Eval(a, e, ctx),
) -> Eval(a, e, ctx) {
use ctx <- Eval
let #(ctx_, result) = eval.run(ctx)
case result {
Ok(a) -> #(ctx_, Ok(a))
Error(e) -> step(f(ctx_, e), ctx)
}
}