Current section
Files
Jump to
Current section
Files
src/smut.gleam
//// Functionally manage shared "mutable" state.
////
//// Obviously, this isn't really shared, mutable state in the classic
//// sense. On the Erlang target, the state lives in its own process,
//// with updates and retrievals happening sequentially through that
//// process's mailbox.
////
//// ```gleam
//// import smut
////
//// let counter = smut.new(0)
//// get(counter) |> echo
//// // 0
//// set(12, counter)
//// get(counter) |> echo
//// // 12
//// ```
import gleam/erlang/process.{type Subject}
// Message types sent to the state handling process.
type Msg(state) {
Assign(state)
Update(fn(state) -> state)
RetrieveThenUpdate(Subject(state), fn(state) -> state)
UpdateThenRetrieve(Subject(state), fn(state) -> state)
Retrieve(Subject(state))
}
/// A handle to the mutable state.
pub opaque type State(state) {
Handle(Subject(Msg(state)))
}
// The function run by the state handling process.
fn run(state: state, pipe: Subject(Msg(state))) -> Nil {
case process.receive_forever(pipe) {
Assign(new_state) -> run(new_state, pipe)
Update(with) -> with(state) |> run(pipe)
RetrieveThenUpdate(return_pipe, with) -> {
process.send(return_pipe, state)
with(state) |> run(pipe)
}
UpdateThenRetrieve(return_pipe, with) -> {
let new_state = with(state)
process.send(return_pipe, new_state)
run(new_state, pipe)
}
Retrieve(return_pipe) -> {
process.send(return_pipe, state)
run(state, pipe)
}
}
}
/// Initialize a new `State`, returning a handle to it.
///
/// ```gleam
/// import gleam/option.{type Option, None, Some}
/// import smut.{type State}
///
/// let global_frog_type: State(Option(String))= smut.new(Some("blue"))
/// smut.get(global_frog_type) |> echo
/// // Some("blue")
/// ```
///
/// ### Note
///
/// On the Erlang target at least, this launches a new process
/// that won't necessarily get cleaned up just because the handle goes
/// out of scope. Normally you'd get a piece of advice here like, "don't call
/// this function in a loop", but Gleam doesn't have loops! But seriously,
/// don't call this in a situation where, in a more imperative language,
/// that call site would be in a loop.
pub fn new(initial_state: state) -> State(state) {
let back_channel: Subject(Subject(Msg(state))) = process.new_subject()
process.spawn(fn() {
let pipe: Subject(Msg(state)) = process.new_subject()
process.send(back_channel, pipe)
run(initial_state, pipe)
})
let pipe = process.receive_forever(back_channel)
Handle(pipe)
}
/// Set the value of the `State`.
///
/// ```gleam
/// import smut.{type State}
///
/// let flavors = ["grape", "banana"] |> smut.new()
/// smut.get(flavors) |> echo
/// // ["grape", "banana"]
/// ["floor stripper", "bog water"] |> smut.set(flavors)
/// smut.get(flavors) |> echo
/// // ["floor stripper", "bog water"]
/// ```
///
/// ## Note
///
/// In a concurrent context, this is probably not the function you want,
/// at least not most of the time. If there is more than one process
/// interacting with your `State`, an update operation like
///
/// ```gleam
/// smut.get(flavors)
/// |> list.prepend("salted caramel")
/// |> smut.set(flavors)
/// ```
///
/// is not guaranteed to be atomic. You almost assuredly want one of
///
/// * [`update()`](#update)
/// * [`get_and_update()`](#get_and_update)
/// * [`update_and_get()`](#update_and_get)
///
/// instead. Updates through these functions happen entirely in the
/// state handling process, and will be atomic.
pub fn set(new: state, handle: State(state)) {
let Handle(send) = handle
process.send(send, Assign(new))
}
/// Return the current value of the state.
///
/// ```gleam
/// import smut
///
/// let cat = smut.new("black")
/// smut.get(cat) |> echo
/// // "black"
/// ```
pub fn get(handle: State(state)) -> state {
let return: Subject(state) = process.new_subject()
let Handle(send) = handle
process.send(send, Retrieve(return))
process.receive_forever(return)
}
/// Update the value of the state atomically, `with` the supplied function.
///
/// ```gleam
/// import smut
///
/// let count = smut.new(0)
/// smut.update(fn(n) { n + 1 }, count)
/// smut.get(count) |> echo
/// // 1, if no other processes have also changed the value between
/// // smut.new() and smut.get()
/// ```
pub fn update(with: fn(state) -> state, handle: State(state)) -> Nil {
let Handle(send) = handle
process.send(send, Update(with))
Nil
}
/// Return the value of the state, then update it atomically `with`
/// the supplied function.
///
/// ```gleam
/// import smut
///
/// let count = smut.new(0)
/// smut.get_and_update(fn(n) { n + 1 }, count) |> echo
/// // 0, if no other processes changed the value between smut.new()
/// // and smut.update()
/// smut.get(count) |> echo
/// // 1, if no other processes changed the value between
/// // smut.get_and_update() and smut.get()
/// ```
pub fn get_and_update(with: fn(state) -> state, handle: State(state)) -> state {
let return: Subject(state) = process.new_subject()
let Handle(send) = handle
process.send(send, RetrieveThenUpdate(return, with))
process.receive_forever(return)
}
/// Update the value of the state `with` the supplied function, _then_
/// return the new updated value.
///
/// ```gleam
/// import smut
///
/// let count = smut.new(0)
/// smut.update_and_get(fn(n), { n + 1 }, count) |> echo
/// // 1, if no other processes changed the value between
/// // smut.new() and smut.update_and_get()
/// smut.get(count) |> echo
/// // 1 again, if no other process changed the value between
/// // smut.update_and_get() and smut.get()
/// ```
pub fn update_and_get(with: fn(state) -> state, handle: State(state)) -> state {
let return: Subject(state) = process.new_subject()
let Handle(send) = handle
process.send(send, UpdateThenRetrieve(return, with))
process.receive_forever(return)
}