defmodule PersistentVector do @moduledoc """ `PersistentVector` is an array-like collection of values indexed by contiguous `0`-based integer index. `PersistentVector` optimizes the following operations: * Get element count * Lookup element by index * Update element by index * Adding new element to the end * Removing element from the end * Enumeration Get count operation is `O(1)`, most others are `O(log32(N))`. `PersistentVector` is implemented as a trie with 32-way branching at each level and uses *structural sharing* for updates. All ideas are borrowed directly from Clojure, yet the implementation (and all the bugs) are my own. ### Supported protocols `PersistentVector` implements the following protocols/behaviors: * `Access` * `Collectable` * `Enumerable` * `Inspect` ## Usage example iex> v = new(1..3) #PersistentVector iex> get(v, 0) 1 iex> v[1] 2 iex> set(v, 1, :two) #PersistentVector iex> v # but v remains unchanged #PersistentVector iex> append(v, 4) #PersistentVector iex> remove_last(v) #PersistentVector ## Efficiency Creating big vectors is OK both CPU-wise and memory-wise. For a `100_000`-element vector the trie depths is 4 *(because `log32(100_000) = 3.3`)*, leading to fast lookup in 4 hops: iex> big = new(100_000..0) iex> big[70_000] 30_000 Update is also fast and efficient as it needs to build only 4 new trie nodes. Apart from that `big1` and `big2` share majority of the elements, leading to efficient memory usage: iex> big1 = new(100_000..0) iex> big2 = set(big1, 70_000, "thirty thousand") iex> big2[70_000] "thirty thousand" """ use Bitwise @shift if Mix.env() == :test, do: 2, else: 5 @block 1 <<< @shift @mask @block - 1 @state __MODULE__ @typep shift :: pos_integer @typedoc "Integer >= 0 for indexing elements." @type index :: non_neg_integer @typedoc "Stored values." @type value :: any @typedoc "The `PersistentVector` itself." @type t :: %__MODULE__{count: index, shift: shift, root: tuple, tail: tuple} defstruct( count: 0, shift: @shift, root: {}, tail: {} ) @compile {:inline, tail_start: 1} @spec tail_start(t) :: index defp tail_start(v = %@state{}), do: v.count - tuple_size(v.tail) @doc "Returns empty `PersistentVector`, same as `new/0`." @spec empty() :: t def empty(), do: %@state{} @doc "Creates new empty `PersistentVector`, same as `empty/0`." @spec new() :: t def new(), do: %@state{} @doc "Returns `PersistentVector` with elements from `enumerable`." @spec new(Enumerable.t) :: t def new(enumerable), do: enumerable |> Enum.reduce(empty(), &(&2 |> append(&1))) @doc "Returns `true` if `v` is empty and `false` otherwise." @spec empty?(t) :: boolean def empty?(v) def empty?(%@state{count: 0}), do: true def empty?(%@state{}), do: false @doc "Returns element count in `v`." @spec count(t) :: index def count(v) def count(%@state{count: count}), do: count @doc """ Returns value of element in `v` at `0`-based `index`. Index must be an integer and satisfy condition `0 <= index < count(v)` or `ArgumentError` will be raised. **Note** that since `PersistentVector` implements `Access` behavior, a shorter syntax `v[i]` can be used. *See also:* `get/3` ## Examples: iex> v = new([:a, :b, :c]) #PersistentVector iex> get(v, 0) :a iex> v[1] :b iex> get(v, 10) ** (ArgumentError) Attempt to get index 10 for vector of size 3 iex> v[10] nil """ @spec get(t, index) :: value | no_return def get(v = %@state{count: count}, index) when is_integer(index) and index >= 0 and index < count do fast_get(v, index) end def get(%@state{count: count}, i) do raise ArgumentError, "Attempt to get index #{inspect i} for vector of size #{count}" end @doc """ Returns value of element in `v` at `0`-based `index` or `default` if `index >= count(v)`. Index must be an integer and satisfy condition `0 <= index` or `ArgumentError` will be raised. *See also:* `get/2` ## Examples: iex> v = new([:a, :b, :c]) #PersistentVector iex> get(v, 0, :not_found) :a iex> get(v, 10, :not_found) :not_found iex> get(v, :bad_index, :not_found) ** (ArgumentError) Attempt to get index :bad_index for vector of size 3 """ @impl Access @spec get(t, index, value) :: value | no_return def get(v = %@state{count: count}, index, _default) when is_integer(index) and index >= 0 and index < count do fast_get(v, index) end def get(%@state{count: count}, index, default) when is_integer(index) and index >= count do default end def get(%@state{count: count}, i, _default) do raise ArgumentError, "Attempt to get index #{inspect i} for vector of size #{count}" end @doc false @compile {:inline, fast_get: 2} @spec fast_get(t, index) :: value def fast_get(v, i) do if i >= tail_start(v) do v.tail else do_get(v.root, v.shift, i) end |> elem(i &&& @mask) end @spec do_get(tuple, shift, index) :: tuple defp do_get(arr, level, i) when level > 0 do arr |> elem((i >>> level) &&& @mask) |> do_get(level - @shift, i) end defp do_get(arr, _level, _i) # when level == 0 do arr end @doc """ Returns last element in `v`, or raises `ArgumentError` if `v` is empty. *See also:* `last/2` ## Examples: iex> v = new(1..3) iex> last(v) 3 iex> last(empty()) ** (ArgumentError) last/1 called for empty vector """ @spec last(t) :: value | no_return def last(v = %@state{count: count}) when count > 0 do v |> fast_get(count - 1) end def last(%@state{}) # when count == 0 do raise ArgumentError, "last/1 called for empty vector" end @doc """ Returns last element in `v`, or `default` if `v` is empty. *See also:* `last/1` ## Examples: iex> v = new(1..3) iex> last(v, nil) 3 iex> last(empty(), nil) nil iex> last(empty(), 0) 0 """ @spec last(t, value) :: value | nil def last(v = %@state{count: count}, _default) when count > 0 do v |> fast_get(count - 1) end def last(%@state{}, default) # when count == 0 do default end @doc """ Returns updated `v` with element at `0`-based `index` set to `new_value`. Index must be an integer and satisfy condition `0 <= index <= count(v)` or `ArgumentError` will be raised. **Note** that setting `index` equal to `count(v)` is allowed and behaves as `append/2`. ## Examples: iex> v = new([:a, :b, :c]) #PersistentVector iex> get(v, 1) :b iex> set(v, 1, :new_value) #PersistentVector iex> set(v, 3, :append) #PersistentVector iex> set(v, 10, :wrong_index) ** (ArgumentError) Attempt to set index 10 for vector of size 3 """ @spec set(t, index, value) :: t | no_return def set(v = %@state{count: count}, index, new_value) when is_integer(index) and index >=0 and index < count do if index >= tail_start(v) do new_tail = v.tail |> put_elem(index &&& @mask, new_value) %{v | tail: new_tail} else new_root = v.root |> do_set(v.shift, index, new_value) %{v | root: new_root} end end def set(v = %@state{count: count}, index, new_value) when is_integer(index) and index == count do v |> append(new_value) end def set(%@state{count: count}, index, _new_value) do raise ArgumentError, "Attempt to set index #{inspect index} for vector of size #{count}" end @spec do_set(tuple, shift, index, value) :: tuple defp do_set(arr, level, i, val) when level > 0 do child_index = (i >>> level) &&& @mask new_child = arr |> elem(child_index) |> do_set(level - @shift, i, val) arr |> put_elem(child_index, new_child) end defp do_set(arr, _level, i, val) # when level == 0 do arr |> put_elem(i &&& @mask, val) end @doc """ Appends `new_value` to the end of `v`. ## Examples: iex> v = append(empty(), 1) #PersistentVector iex> append(v, 2) #PersistentVector """ @spec append(t, value) :: t def append(v = %@state{tail: tail}, new_value) when tuple_size(tail) < @block do new_tail = tail |> Tuple.append(new_value) %{v | count: v.count + 1, tail: new_tail} end def append(v = %@state{}, new_value) do new_count = v.count + 1 new_tail = {new_value} case v.root |> append_block(v.shift, v.tail) do {:ok, new_root} -> %{v | count: new_count, root: new_root, tail: new_tail} {:overflow, tail_path} -> new_root = {v.root, tail_path} %{v | count: new_count, root: new_root, tail: new_tail, shift: v.shift + @shift} end end @spec append_block(tuple, shift, tuple) :: {:ok | :overflow, tuple} defp append_block(arr, level, tail) when level > @shift do last_child_index = tuple_size(arr) - 1 case arr |> elem(last_child_index) |> append_block(level - @shift, tail) do {:ok, new_child} -> {:ok, arr |> put_elem(last_child_index, new_child)} {:overflow, tail_path} -> arr |> append_block_here(tail_path) end end defp append_block(arr, _level, tail) # when level == @shift do arr |> append_block_here(tail) end @compile {:inline, append_block_here: 2} @spec append_block_here(tuple, tuple) :: {:ok | :overflow, tuple} defp append_block_here(arr, tail_path) do if tuple_size(arr) < @block do {:ok, arr |> Tuple.append(tail_path)} else {:overflow, {tail_path}} end end @doc """ Removes last element from `v` or raises `ArgumentError` if `v` is empty. ## Examples: iex> v = new(1..3) #PersistentVector iex> remove_last(v) #PersistentVector iex> remove_last(empty()) ** (ArgumentError) Cannot remove_last from empty vector """ @spec remove_last(t) :: t | no_return def remove_last(v = %@state{tail: tail}) when tuple_size(tail) > 1 do new_tail = tail |> tuple_delete_last() %{v | count: v.count - 1, tail: new_tail} end def remove_last(v = %@state{count: count}) when count > 1 # and tuple_size(tail) == 1 do new_count = v.count - 1 {new_root, new_tail} = remove_last_block(v.root, v.shift) if tuple_size(new_root) == 1 && v.shift > @shift do {new_root} = new_root # remove topmost tree level %{v | count: new_count, root: new_root, shift: v.shift - @shift, tail: new_tail} else %{v | count: new_count, root: new_root, tail: new_tail} end end def remove_last(%@state{count: count}) when count == 1 do empty() end def remove_last(%@state{}) # when count == 0 do raise ArgumentError, "Cannot remove_last from empty vector" end @spec remove_last_block(tuple, shift) :: {tuple, tuple} defp remove_last_block(arr, level) when level > @shift do last_child_index = tuple_size(arr) - 1 case remove_last_block(arr |> elem(last_child_index), level - @shift) do {{}, last_block} -> {arr |> Tuple.delete_at(last_child_index), last_block} {new_child, last_block} -> {arr |> put_elem(last_child_index, new_child), last_block} end end defp remove_last_block(arr, _level) # when level == @shift do last_child_index = tuple_size(arr) - 1 last_block = arr |> elem(last_child_index) new_path = arr |> Tuple.delete_at(last_child_index) {new_path, last_block} end @compile {:inline, tuple_delete_last: 1} @spec tuple_delete_last(tuple) :: tuple defp tuple_delete_last(tuple) do tuple |> Tuple.delete_at(tuple_size(tuple) - 1) end @doc """ Converts `PersistentVector` `v` to `List`. This function is more efficient than `Enum.into/2``(v, [])` call because it builds the list in correct order right away and does not require `:lists.reverse/1` call at the end. ## Examples: iex> to_list(new(1..3)) [1, 2, 3] iex> to_list(empty()) [] """ @spec to_list(t) :: [value] def to_list(v = %@state{count: count}) when count > 0 do Enum.reduce(count-1 .. 0, [], &([v |> fast_get(&1) | &2])) end def to_list(%@state{}) # when count == 0 do [] end @behaviour Access # `get/3` is implemented above @impl Access def fetch(v = %@state{count: count}, key) when is_integer(key) and key >= 0 and key < count do {:ok, v |> fast_get(key)} end def fetch(%@state{}, _key) do :error end @impl Access @spec get_and_update(t, index, fun) :: no_return def get_and_update(%@state{}, _key, _function) do raise UndefinedFunctionError end @impl Access @spec pop(t, index) :: no_return def pop(%@state{}, _key) do raise UndefinedFunctionError end defimpl Enumerable do def count(v = %@for{}), do: {:ok, @for.count(v)} def member?(%@for{}, _element), do: {:error, __MODULE__} def reduce(v = %@for{}, acc, fun) do reduce(v, 0, acc, fun) end defp reduce(v = %@for{count: count}, i, {:cont, acc}, fun) when i < count do reduce(v, i + 1, fun.(v |> @for.fast_get(i), acc), fun) end defp reduce(_v, _i, {:cont, acc}, _fun) do {:done, acc} end defp reduce(_v, _i, {:halt, acc}, _fun) do {:halted, acc} end defp reduce(v, i, {:suspend, acc}, fun) do {:suspended, acc, &reduce(v, i, &1, fun)} end end defimpl Collectable do def into(original) do collector_fun = fn v, {:cont, val} -> v |> @for.append(val) v, :done -> v _, :halt -> :ok end {original, collector_fun} end end defimpl Inspect do import Inspect.Algebra @prefix "#" <> inspect(@for) <> " Integer.to_string(v.count) <> ", ", to_doc(v |> Enum.take(opts.limit + 1), opts), ">"] end end end