lib/arrays_rrb_vector.ex

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