Skuld.Effects.AtomicState (skuld v0.2.3)

View Source

AtomicState effect - thread-safe state for concurrent contexts.

Unlike the regular State effect which stores state in env.state (copied when forking to new processes), AtomicState uses external storage (Agent) that can be safely accessed from multiple processes.

Supports both simple single-state usage and multiple independent states via tags.

Handlers

  • AtomicState.Agent - Agent-backed handler for production (true atomic ops)
  • AtomicState.Sync - State-backed handler for testing (no Agent processes)

Production Usage (Agent handler)

use Skuld.Syntax
alias Skuld.Effects.AtomicState

comp do
  _ <- AtomicState.put(0)
  _ <- AtomicState.modify(&(&1 + 1))
  value <- AtomicState.get()
  value
end
|> AtomicState.Agent.with_handler(0)
|> Comp.run!()
#=> 1

Multiple States (explicit tags)

comp do
  _ <- AtomicState.put(:counter, 0)
  _ <- AtomicState.modify(:counter, &(&1 + 1))
  count <- AtomicState.get(:counter)

  _ <- AtomicState.put(:cache, %{})
  _ <- AtomicState.modify(:cache, &Map.put(&1, :key, "value"))
  cache <- AtomicState.get(:cache)

  {count, cache}
end
|> AtomicState.Agent.with_handler(0, tag: :counter)
|> AtomicState.Agent.with_handler(%{}, tag: :cache)
|> Comp.run!()
#=> {1, %{key: "value"}}

Compare-and-Swap (CAS)

comp do
  _ <- AtomicState.put(10)
  result1 <- AtomicState.cas(10, 20)  # succeeds
  result2 <- AtomicState.cas(10, 30)  # fails - current is 20, not 10
  {result1, result2}
end
|> AtomicState.Agent.with_handler(0)
|> Comp.run!()
#=> {:ok, {:conflict, 20}}

Testing (Sync handler)

For testing without spinning up Agents, use the Sync handler:

comp do
  _ <- AtomicState.put(0)
  _ <- AtomicState.modify(&(&1 + 1))
  AtomicState.get()
end
|> AtomicState.Sync.with_handler(0)
|> Comp.run!()
#=> 1

Summary

Functions

Install AtomicState handler via catch clause syntax.

Returns the env.state key used for storing the Agent pid for a given tag.

Atomically modify the state with a function that returns {result, new_state}.

Compare-and-swap: atomically replace state if it equals expected value.

Atomically read the current state.

Atomically modify the state with a function, returning the new value.

Atomically replace the state.

Returns the env.state key used for State-backed storage for a given tag.

Install an Agent-backed AtomicState handler.

Install a State-backed AtomicState handler for testing.

Functions

__handle__(comp, arg)

Install AtomicState handler via catch clause syntax.

Config selects handler type:

catch
  AtomicState -> {:agent, 0}                    # agent handler
  AtomicState -> {:agent, {0, tag: :counter}}   # agent with opts
  AtomicState -> {:sync, 0}                     # sync handler
  AtomicState -> {:sync, {0, tag: :counter}}    # sync with opts

agent_key(tag)

@spec agent_key(atom()) :: {module(), :agent, atom()}

Returns the env.state key used for storing the Agent pid for a given tag.

atomic_state(fun)

@spec atomic_state((term() -> {term(), term()})) :: Skuld.Comp.Types.computation()

Atomically modify the state with a function that returns {result, new_state}.

Returns the result value.

Examples

AtomicState.atomic_state(fn s -> {:popped, s - 1} end)
AtomicState.atomic_state(:counter, fn s -> {s, s + 1} end)

atomic_state(tag, fun)

@spec atomic_state(atom(), (term() -> {term(), term()})) ::
  Skuld.Comp.Types.computation()

cas(expected, new)

@spec cas(term(), term()) :: Skuld.Comp.Types.computation()

Compare-and-swap: atomically replace state if it equals expected value.

Returns :ok if swap succeeded, {:conflict, current_value} if it failed.

Examples

AtomicState.cas(10, 20)              # if state == 10, set to 20
AtomicState.cas(:counter, 10, 20)    # with explicit tag

cas(tag, expected, new)

@spec cas(atom(), term(), term()) :: Skuld.Comp.Types.computation()

get(tag \\ Skuld.Effects.AtomicState)

Atomically read the current state.

Examples

AtomicState.get()           # use default tag
AtomicState.get(:counter)   # use explicit tag

modify(fun)

@spec modify((term() -> term())) :: Skuld.Comp.Types.computation()

Atomically modify the state with a function, returning the new value.

Examples

AtomicState.modify(&(&1 + 1))              # use default tag
AtomicState.modify(:counter, &(&1 + 1))    # use explicit tag

modify(tag, fun)

@spec modify(atom(), (term() -> term())) :: Skuld.Comp.Types.computation()

put(value)

Atomically replace the state.

Examples

AtomicState.put(42)              # use default tag
AtomicState.put(:counter, 42)    # use explicit tag

put(tag, value)

@spec put(atom(), term()) :: Skuld.Comp.Types.computation()

state_key(tag)

@spec state_key(atom()) :: {module(), :state, atom()}

Returns the env.state key used for State-backed storage for a given tag.

with_agent_handler(comp, initial, opts \\ [])

Install an Agent-backed AtomicState handler.

Delegates to AtomicState.Agent.with_handler/3.

with_state_handler(comp, initial, opts \\ [])

Install a State-backed AtomicState handler for testing.

Delegates to AtomicState.Sync.with_handler/3.