defmodule Skuld.Effects.AtomicState do @moduledoc """ 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 ## Per-tag dispatch Each tag gets its own handler sig (a module atom) and compact operation tuples. The tag is encoded in the sig, not in the operation args: - `get` → `Comp.effect(sig(tag), AtomicState.Get)` — bare atom - `put(v)` → `Comp.effect(sig(tag), {AtomicState.Put, v})` — 2-tuple - `modify(f)` → `Comp.effect(sig(tag), {AtomicState.Modify, f})` - `atomic_state(f)` → `Comp.effect(sig(tag), {AtomicState.AtomicState, f})` - `cas(e, n)` → `Comp.effect(sig(tag), {AtomicState.Cas, e, n})` — 3-tuple """ @behaviour Skuld.Comp.IInstall use Skuld.Comp.DefTaggedOp defoverridable state_key: 1 alias Skuld.Comp.Types @compile {:inline, agent_key: 1, sig: 1} ############################################################################# ## Operations (generated by def_tagged_op) ############################################################################# def_tagged_op get() def_tagged_op put(value) def_tagged_op modify(fun) def_tagged_op atomic_state(fun) def_tagged_op cas(expected, new) # Op atoms as module attributes for use in sub-handlers @get_op @__get_op__ @put_op @__put_op__ @modify_op @__modify_op__ @atomic_state_op @__atomic_state_op__ @cas_op @__cas_op__ ############################################################################# ## Public accessors for sub-handlers ############################################################################# @doc false def get_op, do: @get_op @doc false def put_op, do: @put_op @doc false def modify_op, do: @modify_op @doc false def atomic_state_op, do: @atomic_state_op @doc false def cas_op, do: @cas_op ############################################################################# ## Handler Installation (Delegating) ############################################################################# @doc """ Install an Agent-backed AtomicState handler. Delegates to `AtomicState.Agent.with_handler/3`. """ @spec with_agent_handler(Types.computation(), term(), keyword()) :: Types.computation() defdelegate with_agent_handler(comp, initial, opts \\ []), to: __MODULE__.Agent, as: :with_handler @doc """ Install a State-backed AtomicState handler for testing. Delegates to `AtomicState.Sync.with_handler/3`. """ @spec with_state_handler(Types.computation(), term(), keyword()) :: Types.computation() defdelegate with_state_handler(comp, initial, opts \\ []), to: __MODULE__.Sync, as: :with_handler ############################################################################# ## IInstall Implementation ############################################################################# @doc """ 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 """ @impl Skuld.Comp.IInstall def __handle__(comp, {:agent, {initial, opts}}) when is_list(opts), do: __MODULE__.Agent.with_handler(comp, initial, opts) def __handle__(comp, {:agent, initial}), do: __MODULE__.Agent.with_handler(comp, initial) def __handle__(comp, {:sync, {initial, opts}}) when is_list(opts), do: __MODULE__.Sync.with_handler(comp, initial, opts) def __handle__(comp, {:sync, initial}), do: __MODULE__.Sync.with_handler(comp, initial) ############################################################################# ## Key Helpers ############################################################################# @doc """ Returns the env.state key used for storing the Agent pid for a given tag. """ @spec agent_key(atom()) :: atom() def agent_key(tag), do: sig([:agent, tag]) @doc """ Returns the env.state key used for State-backed storage for a given tag. """ @spec state_key(atom()) :: String.t() def state_key(tag), do: sig([:state, tag]) |> Atom.to_string() end