Elixir by Example

A map is already a store

Builds on MapsModules and Named Functions

Edit any cell and run it in your browser Download source

Across these steps we build KV, a small key-value store, and keep growing it: concurrent, supervised, cached in ETS, expiring keys, and later distributed across nodes. Each step reuses ideas from the concept pages linked above.

Step one: an immutable map already is a store. We only need a module that names the operations, so callers don't depend on the representation.

defmodule KV do
  def new, do: %{}

  def put(store, key, value), do: Map.put(store, key, value)

  def get(store, key, default \\ nil), do: Map.get(store, key, default)

  def delete(store, key), do: Map.delete(store, key)

  def keys(store), do: store |> Map.keys() |> Enum.sort()
end

Every operation returns a new store. Nothing is mutated, so there's no way for two parts of a program to step on each other's data.

store =
  KV.new()
  |> KV.put("lang", "elixir")
  |> KV.put("vm", "beam")
  |> KV.put("year", 2012)

IO.inspect(KV.get(store, "lang"), label: "lang")
IO.inspect(KV.get(store, "missing", :none), label: "missing")
IO.inspect(KV.keys(store), label: "keys")
lang: "elixir"
missing: :none
keys: ["lang", "vm", "year"]

Old versions stay valid. This is what makes immutable data easy to reason about, and later it's what makes undo, snapshots and replication cheap.

older = store
store = KV.delete(store, "year")
IO.inspect(KV.keys(older), label: "older")
IO.inspect(KV.keys(store), label: "now")
older: ["lang", "vm", "year"]
now: ["lang", "vm"]

The catch: a value only lives as long as the variable that holds it. Two processes can't share this store, and nothing outlives the function that created it. In the OTP track, step 4 fixes that by putting the store inside a process.