# Agent-backed handler for AtomicState effect. # # Creates an Agent process that provides true atomic operations accessible # from multiple processes. The Agent is stopped when the computation completes. # # ## Usage # # comp do # _ <- AtomicState.modify(&(&1 + 1)) # AtomicState.get() # end # |> AtomicState.Agent.with_handler(0) # |> Comp.run!() # # ## Multiple Tagged States # # comp do # _ <- AtomicState.modify(:a, &(&1 + 1)) # _ <- AtomicState.modify(:b, &(&1 * 2)) # {AtomicState.get(:a), AtomicState.get(:b)} # end # |> AtomicState.Agent.with_handler(0, tag: :a) # |> AtomicState.Agent.with_handler(10, tag: :b) # |> Comp.run!() defmodule Skuld.Effects.AtomicState.Agent do @moduledoc false @behaviour Skuld.Comp.IHandle @behaviour Skuld.Comp.IInstall alias Skuld.Comp alias Skuld.Comp.Env alias Skuld.Comp.Types alias Skuld.Effects.AtomicState @default_tag AtomicState # Op atoms from parent effect @get_op AtomicState.get_op() @put_op AtomicState.put_op() @modify_op AtomicState.modify_op() @atomic_state_op AtomicState.atomic_state_op() @cas_op AtomicState.cas_op() ############################################################################# ## Handler Installation ############################################################################# @doc """ Install an Agent-backed AtomicState handler. ## Options - `tag` - the state tag (default: `Skuld.Effects.AtomicState`) """ @spec with_handler(Types.computation(), term(), keyword()) :: Types.computation() def with_handler(comp, initial, opts \\ []) do tag = Keyword.get(opts, :tag, @default_tag) agent_key = AtomicState.agent_key(tag) handler_sig = AtomicState.sig(tag) # Op atoms captured for handler closure get_op = @get_op put_op = @put_op modify_op = @modify_op atomic_state_op = @atomic_state_op cas_op = @cas_op handler = fn args, env, k -> case args do ^get_op -> agent = Env.get_state!(env, agent_key) value = Agent.get(agent, & &1) k.(value, env) {^put_op, value} -> agent = Env.get_state!(env, agent_key) Agent.update(agent, fn _ -> value end) k.(:ok, env) {^modify_op, fun} -> agent = Env.get_state!(env, agent_key) new_value = Agent.get_and_update(agent, fn v -> result = fun.(v) {result, result} end) k.(new_value, env) {^atomic_state_op, fun} -> agent = Env.get_state!(env, agent_key) result = Agent.get_and_update(agent, fn v -> {result, new_state} = fun.(v) {result, new_state} end) k.(result, env) {^cas_op, expected, new} -> agent = Env.get_state!(env, agent_key) result = Agent.get_and_update(agent, fn current -> if current == expected do {:ok, new} else {{:conflict, current}, current} end end) k.(result, env) end end comp |> Comp.scoped(fn env -> {:ok, agent} = Agent.start_link(fn -> initial end) # Store agent pid in env.state so handler can find it modified = Env.put_state(env, agent_key, agent) finally_k = fn value, e -> # Stop the agent agent_pid = Env.get_state(e, agent_key) if agent_pid, do: Agent.stop(agent_pid) # Clean up env.state restored_env = %{e | state: Map.delete(e.state, agent_key)} {value, restored_env} end {modified, finally_k} end) |> Comp.with_handler(handler_sig, handler) end ############################################################################# ## IInstall Implementation ############################################################################# @doc """ Install handler via catch clause syntax. catch AtomicState.Agent -> 0 AtomicState.Agent -> {0, tag: :counter} """ @impl Skuld.Comp.IInstall def __handle__(comp, {initial, opts}) when is_list(opts), do: with_handler(comp, initial, opts) def __handle__(comp, initial), do: with_handler(comp, initial) ############################################################################# ## IHandle Implementation (default-tag fallback) ############################################################################# @impl Skuld.Comp.IHandle def handle(op, env, k) do agent_key = AtomicState.agent_key(@default_tag) case op do @get_op -> agent = Env.get_state!(env, agent_key) value = Agent.get(agent, & &1) k.(value, env) {@put_op, value} -> agent = Env.get_state!(env, agent_key) Agent.update(agent, fn _ -> value end) k.(:ok, env) {@modify_op, fun} -> agent = Env.get_state!(env, agent_key) new_value = Agent.get_and_update(agent, fn v -> result = fun.(v) {result, result} end) k.(new_value, env) {@atomic_state_op, fun} -> agent = Env.get_state!(env, agent_key) result = Agent.get_and_update(agent, fn v -> {result, new_state} = fun.(v) {result, new_state} end) k.(result, env) {@cas_op, expected, new} -> agent = Env.get_state!(env, agent_key) result = Agent.get_and_update(agent, fn current -> if current == expected do {:ok, new} else {{:conflict, current}, current} end end) k.(result, env) end end end