//// 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) }