https://sintheta.nexus/blog/feed.xml

Elixir State

2024-10-09

Elixir is blowing my mind :)

State: in an OOP language you'd manage state (let's assume a key-value store) probably via a class and use mutexes and locking for safe access across threads.

In Elixir it's idiomatic to simple use a module, spawn your 'state to be managed' as a process to be sent to and retrieved from.

  • KV is our module
  • start_link/1 spawns a process
  • it starts the function loop/1
    • the loop/1 function is private to the module and can't be used outside the module
    • as argument, it passes an empty map the first time round %{}
  • the loop/1 function itself
    • simply waits for receiving messages
    • the messages are tuples, the first of which an atom specifying the operation
    • via pattern matching different actions are executed based on the received tuple
  • {:get ... -> sends the retrieved value back to the parent process
  • {:put ... -> stores the key-value pair in the map
defmodule KV do 

  def start_link do 
    Task.start_link(fn -> loop(%{}) end)
  end

  defp loop(map) do 
    receive do
      {:get, key, caller} ->
        send(caller, Map.get(map, key))
        loop(map)
      {:put, key, value} ->
        loop(Map.put(map, key, value))
    end
  end

end

{:ok, kvPID} = KV.start_link() 
  • KV is a process which now keeps state (a map)
  • we can send to the KV process (via its PID)
  • we use :put and :get atoms to indicate storage/retrieval
send(kvPID, {:put, "one", 1})
send(kvPID, {:put, "two", 2})

send(kvPID, {:get, "one", self()})

# kvPID will send its response to the parent process

answer = receive do 
  val -> "value retrieved: #{val}"
  nil -> "err: no such key"
after
  500 -> "err: timeout (500ms)"
end

IO.puts answer # value retrieved: 1
  • to reiterate: KV is not a class, we haven't done anything w/ mutexes or locking
  • the map we manage could now be accessed by TENS OF THOUSANDS of processes simultaneously without any race conditions or anything of the sort!

Processes can be spawned in vast numbers, be registered (given a name) for easier referencing, be managed by supervisors to handle faults and respawn, and much more... Highly resilient with minimal syntax and boilerplate! And Elixir supports this process based workflow through and through...

Small example:

# Agents are higher level abstractions around state
# the whole KV functionality can simply be written as

{:ok, pid} = Agent.start_link(fn -> %{} end)

Agent.update(pid, fn map -> Map.put(map, "one", 1) end)
Agent.update(pid, fn map -> Map.put(map, "two", 2) end)

val = Agent.get(pid, fn map -> Map.get(map, "one") end, 500)

IO.puts "value retrieved: #{val}" # value retrieved: 1