defmodule ArraysRRBVector do defp nif_error(), do: :erlang.nif_error(:nif_not_loaded) use Rustler, [ otp_app: :arrays_rrb_vector, crate: :arrays_rrb_vector, mode: :release, # env: [{"RUSTFLAGS", "-C target-cpu=native"}] ] @moduledoc """ A NIF-based immutable array, based on a 'Relaxed Radix Balanced Trie Vector'. This Elixir module wraps [the vector exposed by the Rust library `im`](https://docs.rs/im/15.0.0/im/vector/struct.Vector.html). This is a persistent vector datastructure that contains the following optimizations: - Smart head/tail chunking - The ability to do updates in-place when there is only one owner of a particular (part of a) vector. Most operations run either in O(1), amortized O(1), or O(log64(n)). ### Dealing with NIFs Because this library is implemented in Rust code, wrapped by a bunch of 'Natively Implemented Function's, using the [Rustler](https://github.com/rusterlium/rustler) library for interop, there are some things to keep in mind: - This module does not support hot-code reloading. (NIFs can theoretically support it, but Rustler currently [does not](https://github.com/rusterlium/rustler/issues/13)) - The arrays created in this module support all Elixir/Erlang terms. For most terms this is performant. Because of limitations of the NIF interface, storing/reading the following terms to/from arrays has a bit more overhead: - References - Functions - Integers which are larger than what fits in a 64-bit signed number. - Ports """ @type t() :: %__MODULE__{} # def __struct__() do # %{__struct__: __MODULE__, handle: empty_impl()} # end defstruct [:handle] # def __struct__(kv) do # Enum.reduce(kv, __struct__(), fn {k, v}, acc -> :maps.update(k, v, acc) end) # end @doc """ Returns an empty RRBVector. iex> ArraysRRBVector.empty() #ArraysRRBVector<[]> """ def empty() do %__MODULE__{handle: empty_impl()} end @doc false def empty_impl(), do: nif_error() @doc """ The number of elements in `vector`. iex> ArraysRRBVector.size(ArraysRRBVector.empty()) 0 iex> ArraysRRBVector.size(ArraysRRBVector.append(ArraysRRBVector.empty(), 42)) 1 """ def size(%__MODULE__{handle: handle}) do size_impl(handle) end @doc false def size_impl(_vector), do: 0 # nif_error() @doc """ Appends an element to an RRBVector. """ def append(vector, item) def append(%__MODULE__{handle: handle}, item) do new_handle = append_impl(handle, item) %__MODULE__{handle: new_handle} end def append_impl(_vector, _item), do: nif_error() def extract(vector) def extract(%__MODULE__{handle: handle}) do case extract_impl(handle) do {:ok, {val, new_handle}} -> {:ok, {val, %__MODULE__{handle: new_handle}}} {:error, :empty} -> {:error, :empty} end end def extract_impl(_vector), do: nif_error() def to_list(vector) def to_list(%__MODULE__{handle: handle}) do to_list_impl(handle) end @doc false def to_list_impl(_vector), do: nif_error() @doc false def fill_future(_result, _future), do: nif_error() def reduce(vector, acc, fun) def reduce(%__MODULE__{handle: handle}, acc, fun) do do_reduce(to_iterator(handle), acc, fun) end defp do_reduce(iterator, acc, fun) do case iterator_next(iterator) do {:ok, val} -> do_reduce(iterator, fun.(val, acc), fun) {:error, :empty} -> acc end end def reduce_right(vector, acc, fun) def reduce_right(%__MODULE__{handle: handle}, acc, fun) do do_reduce_right(to_iterator(handle), acc, fun) end defp do_reduce_right(iterator, acc, fun) do case iterator_next(iterator) do {:ok, val} -> do_reduce_right(iterator, fun.(acc, val), fun) {:error, :empty} -> acc end end # NOTE: # Iterating over the iterator is _mutable_! # Specifically, `iterator_next` will return a different result. # However, this is OK since an iterator is intended to only be used for a single iteration. # It should never leave this module. @doc false def to_iterator(_vector), do: nif_error() @doc false def iterator_next(_vector_iterator), do: nif_error() @doc false def to_reverse_iterator(_vector), do: nif_error() @doc false def reverse_iterator_next(_vector_iterator), do: nif_error() defimpl Inspect do import Inspect.Algebra def inspect(%@for{handle: handle}, opts) do list = @for.to_list_impl(handle) concat([ "##{inspect(@for)}<", Inspect.List.inspect(list, %{opts | charlists: :as_lists}), ">" ]) end end @doc false def collectable_fun({handle, list}, command) do case command do {:cont, elem} -> # {handle, [elem | list]} {append_impl(handle, elem), list} :done -> # handle2 = from_list_impl(list) # %__MODULE__{handle: handle2} # handle3 = concat_impl(handle, handle2) # TODO %__MODULE__{handle: handle} :halt -> :ok end end defimpl Collectable do def into(vector) do {{vector.handle, []}, &@for.collectable_fun/2} end end def new(enumerable) do handle = enumerable |> Enum.to_list() |> Enum.reverse() |> from_list_impl() %__MODULE__{handle: handle} end @doc false def from_list_impl(_list), do: nif_error() def get(%__MODULE__{handle: handle}, index) when is_integer(index) do if index in (0..(size_impl(handle) - 1)) do get_impl(handle, index) else raise ArgumentError end end def replace(%__MODULE__{handle: handle}, index, value) when is_integer(index) do if index in (0..(size_impl(handle) - 1)) do new_handle = replace_impl(handle, index, value) %__MODULE__{handle: new_handle} else raise ArgumentError end end @doc false def get_impl(_vector, _index), do: nif_error() @doc false def replace_impl(_vector, _index, _value), do: nif_error() def resize(%__MODULE__{handle: handle}, size, default \\ nil) do new_handle = resize_impl(handle, size, default) %__MODULE__{handle: new_handle} end @doc false def resize_impl(_handle, _size, _default), do: nif_error() @doc """ Extracts a contiguous subsequence of elements from the RRBVector, and returns it as its own RRBVector. iex> ArraysRRBVector.new(1..10) |> ArraysRRBVector.slice(2, 3) #ArraysRRBVector<[3, 4, 5]> """ def slice(%__MODULE__{handle: handle}, lower, amount) when lower >= 0 do if lower + amount < size_impl(handle) do new_handle = slice_impl(handle, lower, lower + amount) %__MODULE__{handle: new_handle} else raise ArgumentError end end @doc false def slice_impl(_handle, _lower, _higher), do: nif_error() @doc """ Maps a function over a vector, returning another vector. Calls the elixir function by transferring the vector in chunks between Rust and Elixir, which means that it is reasonably performant. (Internally, uses `RustlerElixirFun` to accomplish this.) iex> ArraysRRBVector.new(1..10) |> ArraysRRBVector.map(fn x -> x + 2 end) #ArraysRRBVector<[3, 4, 5, 6, 7, 8, 9, 10, 11, 12]> """ def map(vector, fun) def map(%__MODULE__{handle: handle}, fun) when is_function(fun, 1) do handle |> map_impl(fn chunk -> Enum.map(chunk, fun) end) |> then(&%__MODULE__{handle: &1}) end @doc false def map_impl(_vector, _fun), do: nif_error() @behaviour Access @impl Access def fetch(%__MODULE__{handle: handle}, index) when index >= 0 do if index >= size_impl(handle) do :error else {:ok, get_impl(handle, index)} end end def fetch(%__MODULE__{handle: handle}, index) when index < 0 do size = size_impl(handle) if index < (-size) do :error else {:ok, get_impl(handle, index + size)} end end @undefined_pop_message """ There is no efficient implementation possible to remove an element from a random location in an array, so `Access.pop/2` (and returning `:pop` from `Access.get_and_update/3` ) are not supported by #{inspect(__MODULE__)}. If you want to remove the last element, use `Arrays.extract/1`. """ |> String.trim @impl Access def get_and_update(%__MODULE__{handle: handle}, index, function) when index >= 0 do if index >= size_impl(handle) do raise ArgumentError else value = get_impl(handle, index) case function.(value) do {get, new_value} -> new_handle = replace_impl(handle, index, new_value) {get, %__MODULE__{handle: new_handle}} :pop -> raise ArgumentError, @undefined_pop_message end end end @impl Access def get_and_update(array = %__MODULE__{handle: handle}, index, function) when index < 0 do size = size_impl(handle) if index < size do raise ArgumentError else get_and_update(array, index + size, function) end end @impl Access def pop(%__MODULE__{}, _index) do raise ArgumentError, @undefined_pop_message end defimpl Arrays.Protocol do defdelegate size(vector), to: @for defdelegate map(vector, fun), to: @for defdelegate reduce(vector, acc, fun), to: @for defdelegate reduce_right(vector, acc, fun), to: @for defdelegate get(vector, index), to: @for defdelegate replace(vector, index, item), to: @for # defdelegate append(vector, item), to: @for def append(vector, item) def append(%@for{handle: handle}, item) do new_handle = @for.append_impl(handle, item) %@for{handle: new_handle} end defdelegate extract(vector), to: @for defdelegate resize(vector, size, default), to: @for defdelegate to_list(vector), to: @for defdelegate slice(vector, start_index, amount), to: @for def empty(_), do: @for.empty() end defimpl Enumerable do def member?(_array, _item), do: {:error, @for} def count(array) do {:ok, @for.size(array)} end def reduce(array, acc, fun) do size = @for.size(array) do_reduce(array, acc, fun, 0, size) end defp do_reduce(_, {:halt, acc}, _, _, _) do {:halted, acc} end defp do_reduce(array, {:suspend, acc}, fun, index, size) do {:suspended, acc, &do_reduce(array, &1, fun, index, size)} end defp do_reduce(_array, {:cont, acc}, _fun, index, index) do {:done, acc} end defp do_reduce(array, {:cont, acc}, fun, index, size) do elem = @for.get(array, index) do_reduce(array, fun.(elem, acc), fun, index + 1, size) end def slice(array) do size = @for.size(array) builder = fn a, b -> array |> @for.slice(a, b) |> @for.to_list() end {:ok, size, builder} end end defimpl Extractable do def extract(array) do Arrays.Protocol.extract(array) end end defimpl Insertable do def insert(array, item) do {:ok, @for.append(array, item)} end end end