Current section
Files
Jump to
Current section
Files
src/internal/erl_impl.gleam
import gleam/result
import gleam/string
import gleam/erlang/process.{type Subject}
import gleam/otp/actor.{type Next}
import smut/types.{
type SmutError, type TableHandle, type ValueHandle, NewExited, NewFailed,
ValueHandle,
}
const default_timeout: Int = 5000
fn from_start_error(error: actor.StartError) -> SmutError {
case error {
actor.InitTimeout -> NewFailed("timeout")
actor.InitFailed(reason) -> NewFailed(reason)
actor.InitExited(reason) ->
case reason {
process.Normal -> NewExited("process exited normally")
process.Killed -> NewExited("process was killed")
process.Abnormal(reason) -> string.inspect(reason) |> NewExited()
}
}
}
@external(erlang, "erl_shim", "get_table")
fn get_table() -> TableHandle
@external(erlang, "erl_shim", "lookup")
fn lookup_shim(table: TableHandle, key: Int) -> Result(a, Nil)
@external(erlang, "erl_shim", "insert_new")
fn insert_new_shim(table: TableHandle, val: a) -> Int
@external(erlang, "erl_shim", "replace")
fn replace_shim(table: TableHandle, key: Int, val: a) -> Nil
/// Create a table in which to store shared, mutable state, and return a
/// handle to it.
///
/// This should be called as early as possible in your program, and the
/// return value used to instantiate new state slots.
///
/// ```gleam
/// let global_table = smut.init()
/// let counter = smut.new(0)
/// smut.get(counter) |> echo
/// // Ok(0)
/// ```
pub fn init() -> TableHandle {
get_table()
}
type Msg(state) {
Replace(state)
Update(fn(state) -> state)
UpdateThenRetrieve(Subject(state), fn(state) -> state)
RetrieveThenUpdate(Subject(state), fn(state) -> state)
}
/// A handle to an individual shared, mutable value of type `state`.
pub opaque type State(state) {
State(pipe: Subject(Msg(state)), handle: ValueHandle(state), timeout: Int)
}
/// Change the default retrieval timeout for this `state` handle from the
/// default of 5000 ms to `ms` ms.
pub fn with_timeout(state: State(state), ms: Int) -> State(state) {
State(..state, timeout: ms)
}
fn lookup(handle: ValueHandle(a)) -> Result(a, Nil) {
let ValueHandle(table, key) = handle
lookup_shim(table, key)
}
fn replace(handle: ValueHandle(a), new_val: a) -> Nil {
let ValueHandle(table, key) = handle
replace_shim(table, key, new_val)
}
fn insert_new(table: TableHandle, val: a) -> ValueHandle(a) {
let key = insert_new_shim(table, val)
ValueHandle(table, key)
}
fn run_actor(
handle: ValueHandle(state),
message: Msg(state),
) -> Next(ValueHandle(state), Msg(state)) {
case message {
Replace(new_val) -> {
replace(handle, new_val)
actor.continue(handle)
}
Update(f) ->
case lookup(handle) {
Ok(val) -> {
f(val) |> replace(handle, _)
actor.continue(handle)
}
Error(_) -> actor.continue(handle)
}
UpdateThenRetrieve(return, f) ->
case lookup(handle) {
Ok(val) -> {
let new_val = f(val)
replace(handle, new_val)
process.send(return, new_val)
actor.continue(handle)
}
Error(_) -> actor.continue(handle)
}
RetrieveThenUpdate(return, f) ->
case lookup(handle) {
Ok(val) -> {
process.send(return, val)
f(val) |> replace(handle, _)
actor.continue(handle)
}
Error(_) -> actor.continue(handle)
}
}
}
/// Create a new slot for a shared, mutable value of type `state`, and
/// return a handle to it, through which reads, writes, and updates can
/// be performed.
pub fn new(
table: TableHandle,
initial_val: state,
) -> Result(State(state), SmutError) {
let value_handle = insert_new(table, initial_val)
use actor <- result.try(
actor.new(value_handle)
|> actor.on_message(run_actor)
|> actor.start
|> result.map_error(from_start_error),
)
State(actor.data, value_handle, default_timeout) |> Ok()
}
/// Get the current value.
pub fn get(state: State(state)) -> Result(state, Nil) {
lookup(state.handle)
}
/// Set the value of the state to `new_val`.
///
/// The majority of the time, you will probably want to use one of
///
/// * `update()`
/// * `update_and_retrieve()`
/// * `retrieve_and_update()`
///
/// instead. An operation like
/// ```gleam
/// let assert Ok(cur_val) = smut.get(state)
/// let new_val = update_state(cur_val)
/// smut.set(state, new_val)
/// ```
/// is not guaranteed to be atomic. Another process could futz with the value
/// of `state` between the call to `smut.get()` and `smut.set()`. The `update`
/// family of functions ensure atomic updates.
pub fn set(new_val: state, state: State(state)) -> Nil {
process.send(state.pipe, Replace(new_val))
}
/// Update the current value of `state` with the given update function.
///
/// This operation is guaranteed to be atomic; that is, no other process
/// can affect the value of `state` between its retrieval and replacement.
pub fn update(with: fn(state) -> state, state: State(state)) -> Nil {
process.send(state.pipe, Update(with))
}
pub fn update_and_retrieve(
with: fn(state) -> state,
state: State(state),
) -> Result(state, Nil) {
let return = process.new_subject()
process.send(state.pipe, UpdateThenRetrieve(return, with))
process.receive(return, state.timeout)
}
pub fn retrieve_and_update(
with: fn(state) -> state,
state: State(state),
) -> Result(state, Nil) {
let return = process.new_subject()
process.send(state.pipe, RetrieveThenUpdate(return, with))
process.receive(return, state.timeout)
}