Elixir by Example

Many buckets

Builds on EnumThe Pipe OperatorProtocols

Edit any cell and run it in your browser Download source

One flat namespace gets crowded. Step 3 groups keys into named buckets (a map of maps) and adds a protocol so the store can be exported to a simple text format without KV knowing every value type.

defmodule KV do
  defstruct buckets: %{}

  def put(%KV{} = kv, bucket, key, value) do
    %{kv | buckets: Map.update(kv.buckets, bucket, %{key => value}, &Map.put(&1, key, value))}
  end

  def get(%KV{buckets: b}, bucket, key), do: b |> Map.get(bucket, %{}) |> Map.get(key)

  def bucket_names(%KV{buckets: b}), do: b |> Map.keys() |> Enum.sort()

  # Bulk loading is a reduce: fold every row into the
  # store, starting from an empty one.
  def load(rows), do: Enum.reduce(rows, %KV{}, fn {b, k, v}, kv -> put(kv, b, k, v) end)
end

A protocol is an interface dispatched on the data type. Anyone can implement it for their own types, later, without touching KV.

defprotocol KV.Codec do
  @doc "Encodes a value as a single line of text."
  def encode(value)
end

defimpl KV.Codec, for: BitString do
  def encode(s), do: inspect(s)
end

defimpl KV.Codec, for: Integer do
  def encode(n), do: Integer.to_string(n)
end

defimpl KV.Codec, for: List do
  def encode(list), do: "[" <> Enum.map_join(list, ",", &KV.Codec.encode/1) <> "]"
end

rows = [
  {"users", "ada", "admin"},
  {"users", "grace", "editor"},
  {"stats", "visits", 42},
  {"stats", "top", ["/", "/docs"]}
]

kv = KV.load(rows)
IO.inspect(KV.bucket_names(kv), label: "buckets")
IO.inspect(KV.get(kv, "users", "grace"), label: "users/grace")
IO.inspect(KV.get(kv, "nope", "x"), label: "missing bucket")
buckets: ["stats", "users"]
users/grace: "editor"
missing bucket: nil

Export with an Enum pipeline: flatten buckets into rows, sort them, encode each value via the protocol.

kv.buckets
|> Enum.flat_map(fn {bucket, data} -> Enum.map(data, fn {k, v} -> {bucket, k, v} end) end)
|> Enum.sort()
|> Enum.map(fn {b, k, v} -> "#{b}/#{k} = #{KV.Codec.encode(v)}" end)
|> Enum.each(&IO.puts/1)
stats/top = ["/","/docs"]
stats/visits = 42
users/ada = "admin"
users/grace = "editor"

Values without an implementation fail loudly, which is better than writing something unreadable.

try do
  KV.Codec.encode(%{a: 1})
rescue
  e in Protocol.UndefinedError -> IO.puts("no codec for #{inspect(e.value)}")
end
no codec for %{a: 1}

Still a value in a variable: one process owns it, nobody else can see updates. Step 4 (OTP track) moves the store into a process everyone can call.