act

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 type.

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)

Functions

pub fn all(
  actions: List(fn(a) -> #(a, b)),
) -> fn(a) -> #(a, List(b))

Run a list of actions in sequence, returning a list of the results.

pub fn do(
  first_do: fn(a) -> #(a, b),
  and_then: fn(b) -> fn(a) -> #(a, c),
) -> fn(a) -> #(a, c)

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 each(
  actions: List(fn(a) -> #(a, b)),
) -> fn(a) -> #(a, Nil)

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 error(value: a) -> fn(b) -> #(b, Result(c, a))

Create an action that returns the given value wrapped in an Error.

pub fn eval(action: fn(a) -> #(a, b), with state: a) -> b

Run an action with the given state and return its result. This function is the equivalent of action(state).1 and may help improve readability.

pub fn exec(action: fn(a) -> #(a, b), with state: a) -> a

Run an action with the given state and return the final state, ignoring the action’s result. This function is the equivalent of action(state).0 and may help improve readability.

pub fn get_state() -> fn(a) -> #(a, a)

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 map(
  action: fn(a) -> #(a, b),
  f: fn(b) -> c,
) -> fn(a) -> #(a, c)

Transform the value produced by an action with the given function.

pub fn map_error(
  action: fn(a) -> #(a, Result(b, c)),
  f: fn(c) -> d,
) -> fn(a) -> #(a, Result(b, d))

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_ok(
  action: fn(a) -> #(a, Result(b, c)),
  f: fn(b) -> d,
) -> fn(a) -> #(a, Result(d, c))

Transform the value produced by an action with the given function if it is wrapped in an Ok, returning the Error otherwise.

pub fn ok(value: a) -> fn(b) -> #(b, Result(a, c))

Create an action that returns the given value wrapped in an Ok.

pub fn return(result: a) -> fn(b) -> #(b, a)

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 run(action: fn(a) -> #(a, b), with state: a) -> #(a, b)

Run an action with the given state. Since actions are just functions that can be called like any other, you will typically never need this function except to improve readability in situations where it’s not obvious what’s going on.

pub fn set_state(state: a) -> fn(a) -> #(a, Nil)

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 try(
  first_try: fn(a) -> #(a, Result(b, c)),
  and_then: fn(b) -> fn(a) -> #(a, Result(d, c)),
) -> fn(a) -> #(a, Result(d, c))

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_all(
  actions: List(fn(a) -> #(a, Result(b, c))),
) -> fn(a) -> #(a, Result(List(b), c))

Run a list of actions in sequence, stopping if an Error is encountered, and returning a list of the results.

pub fn try_each(
  actions: List(fn(a) -> #(a, Result(b, c))),
) -> fn(a) -> #(a, Result(Nil, c))

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 update_state(updater: fn(a) -> a) -> fn(a) -> #(a, 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 })
}
Search Document