Packages

Compose stateful actions to simulate mutable state

Current section

Files

Jump to
act src act.gleam
Raw

src/act.gleam

//// Gleam is a functional programming language that does not support having
//// *mutable state*. As such, programmers often have to pass state around manually,
//// threading it through functions via arguments and return values. This can
//// become a bit repetitive and clumsy.
////
//// What if state could be 'threaded' through functions automatically, with a
//// nice API that resembles mutable state? This is the central idea of `act` and
//// the [`Action`](#Action) type.
////
// ---- IMPORTS ----------------------------------------------------------------
import gleam/list
// ---- TYPES ------------------------------------------------------------------
/// An action is simply a function that takes some state and returns a value and
/// a potentially updated state. Running an action is as simple as calling the
/// function with a state.
///
/// ```
/// import gleam/int
/// import act.{type Action}
///
/// fn increment(by num: Int) -> Action(String, Int) {
/// fn(state) {
/// #(state + num, "I added " <> int.to_string(by))
/// }
/// }
///
/// pub fn main() {
/// let initial_state = 0
///
/// initial_state
/// |> act.all([increment(by: 2), increment(by: 5), increment(by: 1)])
/// }
///
/// // -> #(8, ["I added 2", "I added 5", "I added 1"])
/// ```
///
/// As you can see, actions really are *just functions*! `act` simply provides a
/// nice API for creating and working with these functions.
///
pub type Action(result, state) =
fn(state) -> #(state, result)
/// An action that returns a `Result`, meaning it may fail.
///
pub type ResultAction(ok, error, state) =
Action(Result(ok, error), state)
// ---- CONSTRUCTORS -----------------------------------------------------------
/// Create an action that returns the given value and doesn't modify state.
///
/// ```
/// fn foo() -> Action(String, s) {
/// use _ <- do(update_something())
/// return("Updated!")
/// }
/// ```
///
pub fn return(result: result) -> Action(result, state) {
fn(state) { #(state, result) }
}
/// Create an action that returns the given value wrapped in an `Ok`.
///
pub fn ok(value: ok) -> ResultAction(ok, error, state) {
fn(state) { #(state, Ok(value)) }
}
/// Create an action that returns the given value wrapped in an `Error`.
///
pub fn error(value: error) -> ResultAction(ok, error, state) {
fn(state) { #(state, Error(value)) }
}
// ---- STATE ------------------------------------------------------------------
/// Create an action that returns the current state. This is useful because
/// functions such as `do` do not pass the updated state to their callbacks.
///
/// ```
/// fn foo() {
/// use original_state <- do(get_state())
/// use result <- do(some_action)
/// use new_state <- do(get_state())
/// // do something with the variables
/// }
/// ```
///
pub fn get_state() -> Action(state, state) {
fn(state) { #(state, state) }
}
/// Create an action that sets the current state to a new value, returning `Nil`.
///
/// ```
/// fn set_to_42() -> Action(String, Int) {
/// use Nil <- do(set_state(42))
/// return("The state is now 42! HAHAHAHA!!!")
/// }
/// ```
///
pub fn set_state(state: state) -> Action(Nil, state) {
fn(_) { #(state, Nil) }
}
/// Create an action that updates the current state with the given function and
/// returns `Nil`.
///
/// ```
/// fn increment_state(by: Int) -> Action(Nil, Int) {
/// update_state(fn(s) { s + by })
/// }
/// ```
///
pub fn update_state(updater: fn(state) -> state) -> Action(Nil, state) {
fn(state) { #(updater(state), Nil) }
}
// ---- MANIPULATIONS ----------------------------------------------------------
/// Transform the value produced by an action with the given function.
///
pub fn map(action: Action(a, state), f: fn(a) -> b) -> Action(b, state) {
fn(state) {
let #(state, result) = action(state)
#(state, f(result))
}
}
/// Transform the value produced by an action with the given function if it is
/// wrapped in an `Ok`, returning the `Error` otherwise.
///
pub fn map_ok(
action: ResultAction(a, error, state),
f: fn(a) -> b,
) -> ResultAction(b, error, state) {
fn(state) {
let #(state, result) = action(state)
case result {
Ok(a) -> #(state, Ok(f(a)))
Error(e) -> #(state, Error(e))
}
}
}
/// Transform the error produced by an action with the given function if it is
/// wrapped in an `Error`, returning the `Ok` value otherwise.
///
pub fn map_error(
action: ResultAction(ok, a, state),
f: fn(a) -> b,
) -> ResultAction(ok, b, state) {
fn(state) {
let #(state, result) = action(state)
case result {
Error(e) -> #(state, Error(f(e)))
Ok(a) -> #(state, Ok(a))
}
}
}
// ---- COMBINATORS ------------------------------------------------------------
/// Run the first action, passing its result to the `and_then` function which
/// returns another action. This is very useful for chaining multiple actions
/// together with `use` expressions.
///
/// ```
/// fn foo() {
/// use a_result <- do(some_action)
/// use another_result <- do(another_action("blah"))
/// return(a_result <> another_result)
/// }
/// ```
///
/// Using `use` is of course optional.
///
/// ```
/// fn bar() {
/// do(some_action, fn(result) {
/// io.debug(result)
/// return(result)
/// })
/// }
/// ```
///
pub fn do(
first_do: Action(a, state),
and_then: fn(a) -> Action(b, state),
) -> Action(b, state) {
fn(state) {
let #(state, result) = first_do(state)
and_then(result)(state)
}
}
/// Like a combination of `do` and `result.try`. If the first action returns an
/// `Ok` value, the `and_then` function is called with that value and the action
/// that it returns is run. If the first action returns an `Error` value, the
/// `and_then` function is not called and the error is returned.
///
pub fn try(
first_try: ResultAction(a, error, state),
and_then: fn(a) -> ResultAction(b, error, state),
) -> ResultAction(b, error, state) {
fn(state) {
let #(state, result) = first_try(state)
case result {
Ok(a) -> and_then(a)(state)
Error(e) -> #(state, Error(e))
}
}
}
/// Run a list of actions in sequence, returning a list of the results.
///
pub fn all(actions: List(Action(result, state))) -> Action(List(result), state) {
list.map_fold(actions, _, fn(state, action) { action(state) })
}
/// Run a list of actions in sequence purely for updating state, ignoring their
/// results. This function runs faster than `all` since it doesn't have to
/// traverse the result list.
///
pub fn each(actions: List(Action(result, state))) -> Action(Nil, state) {
fn(state) {
#(list.fold(actions, state, fn(state, action) { action(state).0 }), Nil)
}
}
/// Run a list of actions in sequence, stopping if an `Error` is encountered,
/// and returning a list of the results.
///
pub fn try_all(
actions: List(ResultAction(ok, error, state)),
) -> ResultAction(List(ok), error, state) {
fn(state) {
list.fold_until(actions, #(state, Ok([])), fn(acc, action) {
let assert #(state, Ok(results)) = acc
case action(state) {
#(new_state, Ok(result)) ->
list.Continue(#(new_state, Ok([result, ..results])))
#(new_state, Error(result)) -> list.Stop(#(new_state, Error(result)))
}
})
}
|> map_ok(list.reverse)
}
/// Run a list of actions in sequence purely for updating state, stopping if an
/// `Error` is encountered. This function runs faster than `try_all` since it
/// doesn't have to traverse the result list.
///
pub fn try_each(
actions: List(ResultAction(ok, error, state)),
) -> ResultAction(Nil, error, state) {
fn(state) {
list.fold_until(actions, #(state, Ok(Nil)), fn(acc, action) {
let #(state, nil_result) = acc
case action(state) {
#(new_state, Ok(_)) -> list.Continue(#(new_state, nil_result))
#(new_state, Error(result)) -> list.Stop(#(new_state, Error(result)))
}
})
}
}