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() -> Action(String, state) {
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() -> Action(result, state) {
do(some_action, fn(result) {
io.debug(result)
return(result)
})
}
💡 If you find yourself combining _state
functions with do
a lot, check
out the state
module!
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 flatten(
action: fn(a) -> #(a, fn(a) -> #(a, b)),
) -> fn(a) -> #(a, b)
Merges a nested Action
into a single layer.
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() -> Action(result, state) {
use original_state <- do(get_state())
use result <- do(some_action)
use new_state <- do(get_state())
// do something with the variables
}
💡 If you find yourself combining _state
functions with do
a lot, check
out the state
module!
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!!!")
}
💡 If you find yourself combining _state
functions with do
a lot, check
out the state
module!
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 })
}
💡 If you find yourself combining _state
functions with do
a lot, check
out the state
module!