defmodule Idx do @moduledoc """ Collection allowing access via dynamically created indices. The API is similar to `Map`. See `create_index/3` for more details about indices. iex> users = [%{name: "Bob", age: 20}, %{name: "Eve", age: 27}, %{name: "John", age: 45}] iex> idx = Idx.new(users, & &1.name) iex> Idx.get(idx, "Bob") %{name: "Bob", age: 20} iex> idx |> Enum.to_list() |> Enum.sort() users """ alias __MODULE__.{Key, Primary} @behaviour Access @enforce_keys [Primary, :indices, :lazy_indices] defstruct @enforce_keys @type t :: %{:__struct__ => __MODULE__, any => any} @typedoc """ Index function. Returns a key based on the value to be indexed. Must be pure (always return the same key for the same value). """ @type index :: (value -> key) @type index_name :: atom @type key :: any @type value :: any @type full_key :: primary_key | non_primary_key @type primary_key :: key @opaque non_primary_key :: {Key, index_name, key} @doc """ Creates a new Idx instance. """ @spec new(Enumerable.t(), index) :: t def new(enum \\ [], primary_index) do map = Map.new(enum, &{{Primary, primary_index.(&1)}, &1}) %__MODULE__{Primary => primary_index, indices: %{}, lazy_indices: %{}} |> Map.merge(map) end @doc """ Allows accessing a value by a non-primary key in the idx. See `create_index/3` for details. """ @spec key(index_name, non_primary_key) :: full_key def key(index_name, key) do {Key, index_name, key} end @doc """ Creates a new non-primary index on the `Idx` instance. iex> users = [%{name: "Bob", age: 20}, %{name: "Eve", age: 27}, %{name: "John", age: 45}] iex> idx = Idx.new(users, & &1.name) iex> idx = Idx.create_index(idx, :initial, &String.first(&1.name)) iex> Idx.get(idx, Idx.key(:initial, "J")) %{name: "John", age: 45} If `lazy` option is set to true, the index keys won't be precomputed and stored, instead it will always be calculated on demand. This will slow down access, but speed up insertion and operations relying on other indices. """ @spec create_index(t, index_name, index, lazy?: boolean()) :: t def create_index(%__MODULE__{} = idx, name, fun, options \\ []) do %__MODULE__{indices: indices, lazy_indices: lazy_indices} = idx if Map.has_key?(indices, name) or Map.has_key?(lazy_indices, name) do raise ArgumentError, "Index #{inspect(name)} already present" end if Keyword.get(options, :lazy?, false) do %{idx | lazy_indices: Map.put(lazy_indices, name, fun)} else data = idx |> to_map() |> Map.new(fn {key, value} -> {fun.(value), key} end) %{idx | indices: Map.put(indices, name, fun)} |> Map.put({__MODULE__, name}, data) end end @spec drop_index(t, index_name) :: t def drop_index(%__MODULE__{} = idx, name) do %__MODULE__{indices: indices, lazy_indices: lazy_indices} = idx {eager_fun, indices} = Map.pop(indices, name) {lazy_fun, lazy_indices} = Map.pop(lazy_indices, name) unless eager_fun || lazy_fun do raise ArgumentError, "Unknown index #{inspect(name)}" end idx = Map.delete(idx, {__MODULE__, name}) %{idx | indices: indices, lazy_indices: lazy_indices} end @spec put(t, value) :: t def put(%__MODULE__{Primary => primary_index, indices: indices} = idx, value) when indices == %{} do Map.put(idx, {Primary, primary_index.(value)}, value) end def put(%__MODULE__{Primary => primary_index, indices: indices} = idx, value) do primary_key = primary_index.(value) idx = Map.put(idx, {Primary, primary_index.(value)}, value) Enum.reduce(indices, idx, fn {name, fun}, idx -> name = {__MODULE__, name} %{^name => data} = idx %{idx | name => Map.put(data, fun.(value), primary_key)} end) end @spec fetch(t, full_key) :: {:ok, value} | :error def fetch(%__MODULE__{} = idx, key) do key = resolve_key(idx, key) case idx do %{^key => value} -> {:ok, value} %{} -> :error end end @spec fetch!(t, full_key) :: value def fetch!(idx, key) do key = resolve_key!(idx, key) %{^key => value} = idx value end @spec get(t, full_key, value | nil) :: value | nil def get(idx, key, default \\ nil) do case fetch(idx, key) do {:ok, value} -> value :error -> default end end @spec pop!(t, full_key) :: value def pop!(idx, key) do key = resolve_key!(idx, key) {value, idx} = Map.pop!(idx, key) {value, remove_value_from_indices(idx, value)} end def pop(idx, key, default \\ nil) do key = resolve_key(idx, key) case :maps.take(key, idx) do {value, idx} -> {value, remove_value_from_indices(idx, value)} :error -> {default, idx} end end defp remove_value_from_indices(%Idx{indices: indices} = idx, value) do Enum.reduce(indices, idx, fn {name, fun}, idx -> name = {__MODULE__, name} %{^name => data} = idx key = fun.(value) data = Map.delete(data, key) %{idx | name => data} end) end @spec update!(t, full_key, (value -> value)) :: t def update!(idx, key, fun) do key = resolve_key!(idx, key) {value, idx} = pop!(idx, key) put(idx, fun.(value)) end @doc """ Updates the value under the key by calling `fun`. No indices can change after the update. Thanks to the guarantee that the indices (including the primary index) won't change, this function is faster than `update!/3`. However, if the indices change, it will leave the `idx` in an invalid state and lead to undefined behaviour. """ @spec fast_update!(t, full_key, (value -> value)) :: t def fast_update!(idx, key, fun) do key = resolve_key!(idx, key) Map.update!(idx, key, fun) end @spec get_and_update!(t, full_key, (value -> {get, update} | :pop)) :: {get, t} when get: any, update: value def get_and_update!(idx, key, fun) do key = resolve_key!(idx, key) {value, idx} = pop!(idx, key) case fun.(value) do {get, update} -> {get, put(idx, update)} :pop -> {value, idx} end end @spec get_and_update(t, full_key, (value | nil -> {get, update} | :pop)) :: {get, t} when get: any, update: value def get_and_update(idx, key, fun) do key = resolve_key(idx, key) {value, idx} = pop(idx, key) case fun.(value) do {get, update} -> {get, put(idx, update)} :pop -> {value, idx} end end @spec size(t) :: non_neg_integer def size(%Idx{indices: indices} = idx) do map_size(idx) - map_size(Idx.__struct__()) - map_size(indices) end @spec to_list(t) :: [value()] def to_list(%Idx{} = idx) do :maps.fold( fn {Primary, _key}, value, acc -> [value | acc] _key, _value, acc -> acc end, [], idx ) |> :lists.reverse() end @doc """ Converts `idx` to a map, where keys are the primary keys of the `idx`. """ @spec to_map(t) :: %{key => value} def to_map(%Idx{} = idx) do Enum.reduce(Map.from_struct(idx), %{}, fn {{Primary, key}, value}, acc -> Map.put(acc, key, value) _kv, acc -> acc end) end @spec member?(t, value) :: boolean def member?(%Idx{Idx.Primary => primary} = idx, value) do {:ok, value} == Idx.fetch(idx, primary.(value)) end @spec primary_key!(t, index_name, non_primary_key) :: primary_key def primary_key!(%{__struct__: Idx} = idx, name, key) do name = {__MODULE__, name} %{^name => %{^key => primary}} = idx primary end @doc """ Returns the primary key of a value under the given non-primary key. """ @spec primary_key(t, index_name, non_primary_key) :: {:ok, primary_key} | :error def primary_key(%Idx{} = idx, name, key) do name = {__MODULE__, name} case idx do %{^name => %{^key => primary}} -> {:ok, primary} %{} -> :error end end defp resolve_key(_idx, {Primary, primary}) do {Primary, primary} end defp resolve_key(idx, {Key, name, key}) do data_ref = {__MODULE__, name} case idx do %{^data_ref => %{^key => primary}} -> {Primary, primary} %{lazy_indices: %{^name => fun}} -> resolve_key_lazy(idx, fun, key) %{} -> Key.Imaginary end end defp resolve_key(_idx, primary) do {Primary, primary} end defp resolve_key!(_idx, {Primary, primary}) do {Primary, primary} end defp resolve_key!(idx, {Key, name, key}) do case resolve_key(idx, {Key, name, key}) do Key.Imaginary -> raise "Unknown key #{inspect(key)} of index #{inspect(name)}" primary_key -> primary_key end end defp resolve_key!(_idx, primary) do {Primary, primary} end defp resolve_key_lazy(idx, fun, key) do Enum.find_value(Map.from_struct(idx), Key.Imaginary, fn {{Primary, primary_key}, value} -> if fun.(value) === key, do: {Primary, primary_key} _entry -> nil end) end end defimpl Enumerable, for: Idx do def count(idx) do {:ok, Idx.size(idx)} end def member?(idx, value) do {:ok, Idx.member?(idx, value)} end def slice(idx) do size = Idx.size(idx) {:ok, size, &Idx.to_list/1} end def reduce(idx, acc, fun) do Enumerable.List.reduce(Idx.to_list(idx), acc, fun) end end defimpl Collectable, for: Idx do def into(idx) do fun = fn idx, {:cont, value} -> Idx.put(idx, value) idx, :done -> idx _idx, :halt -> :ok end {idx, fun} end end defimpl Inspect, for: Idx do import Inspect.Algebra def inspect(%Idx{Idx.Primary => primary} = idx, opts) do opts = %Inspect.Opts{opts | charlists: :as_lists} concat([ "#Idx<", to_doc(Idx.to_list(idx), opts), ", ", "indices: ", to_doc( [primary: primary] ++ Enum.to_list(idx.indices) ++ Enum.to_list(idx.lazy_indices), opts ), ">" ]) end end