Current section
Files
Jump to
Current section
Files
lib/ets_deque.ex
defmodule EtsDeque do
@moduledoc """
EtsDeque is an Elixir implementation of a double-ended queue (deque), using
Erlang's ETS library as a backing store.
Using ETS ensures that all functions in the `EtsDeque` module execute in
amortized O(1) time with a minimum of memory allocations, offering bounded
or unbounded operation with high performance and favorable RAM usage.
Using ETS also means that `EtsDeque` is not a purely functional data
structure, and is not suitable for direct concurrent usage in multiple
processes. Use the `EtsDeque.Server` GenServer if you would like safe
access to an `EtsDeque` from multiple processes.
You can push items onto, pop items from, or peek at items from the head
or tail of the queue. Additionally, any item can be accessed or replaced
by its index using `at/2` and `replace_at/3`.
`EtsQueue` implements Elixir's
[Access](https://hexdocs.pm/elixir/Access.html) behaviour and
[Enumerable](https://hexdocs.pm/elixir/Enumerable.html) and
[Collectable](https://hexdocs.pm/elixir/Collectable.html) protocols,
so code like `deque[0]` and `Enum.count(deque)` and
`Enum.into([1, 2, 3], EtsDeque.new())` works as it should.
## Example
iex> deque = EtsDeque.new(3)
iex> {:ok, deque} = EtsDeque.push_head(deque, :moe)
iex> {:ok, deque} = EtsDeque.push_tail(deque, :larry)
iex> {:ok, deque} = EtsDeque.push_tail(deque, :curly)
iex> :error = EtsDeque.push_head(deque, :shemp) ## deque is full
iex> {:ok, :curly, deque} = EtsDeque.pop_tail(deque)
iex> {:ok, deque} = EtsDeque.push_tail(deque, :shemp)
iex> Enum.to_list(deque)
[:moe, :larry, :shemp]
"""
defstruct [:table, :size, :length, :head]
@type t :: %__MODULE__{}
@doc ~S"""
Creates a deque, optionally limited to a given size.
"""
@spec new(non_neg_integer | :infinity) :: t()
def new(size \\ :infinity) do
table = :ets.new(nil, [:set, :public])
%__MODULE__{table: table, size: size, length: 0, head: -1}
end
@doc ~S"""
Returns the number of items in the given deque. Equivalent to `deque.length`.
"""
@spec length(t) :: non_neg_integer
def length(deque), do: deque.length
@doc ~S"""
Returns the maximum capacity of the given deque. Equivalent to `deque.size`.
"""
@spec size(t) :: non_neg_integer | :infinity
def size(deque), do: deque.size
@doc ~S"""
Adds an item onto the head of the queue. Returns the updated deque,
or `:error` if the queue is full.
"""
@spec push_head(t, any) :: {:ok, t} | :error
def push_head(deque, item) do
if deque.length + 1 > deque.size do
:error
else
new_head = new_head(deque, 1)
true = :ets.insert(deque.table, {new_head, item})
{:ok, %{deque | head: new_head, length: deque.length + 1}}
end
end
@doc ~S"""
Adds an item onto the head of the queue. Returns the updated deque,
or raises `ArgumentError` if the queue is full.
"""
@spec push_head!(t, any) :: t
def push_head!(deque, item) do
case push_head(deque, item) do
{:ok, deque} -> deque
:error -> raise ArgumentError, "deque is full"
end
end
@doc ~S"""
Adds an item onto the tail of the queue. Returns the updated deque,
or `:error` if the queue is full.
"""
@spec push_tail(t, any) :: {:ok, t} | :error
def push_tail(deque, item) do
if deque.length + 1 > deque.size do
:error
else
tail = tail(deque, 1)
head = if deque.length == 0, do: tail, else: deque.head
true = :ets.insert(deque.table, {tail, item})
{:ok, %{deque | length: deque.length + 1, head: head}}
end
end
@doc ~S"""
Adds an item onto the tail of the queue. Returns the updated deque,
or raises `ArgumentError` if the queue is full.
"""
@spec push_tail!(t, any) :: t
def push_tail!(deque, item) do
case push_tail(deque, item) do
{:ok, deque} -> deque
:error -> raise ArgumentError, "deque is full"
end
end
@doc ~S"""
Removes the item at the head of the queue, returning it along with the
updated deque.
Returns `:error` if queue is empty.
"""
@spec pop_head(t) :: {:ok, any, t} | :error
def pop_head(deque) do
if deque.length == 0 do
:error
else
[{_, item}] = :ets.take(deque.table, deque.head)
new_head = new_head(deque, -1)
new_deque = %{deque | length: deque.length - 1, head: new_head}
{:ok, item, new_deque}
end
end
@doc ~S"""
Removes the item at the head of the queue, returning it along with the
updated deque.
Raises `ArgumentError` if queue is empty.
"""
@spec pop_head!(t) :: {any, t}
def pop_head!(deque) do
case pop_head(deque) do
{:ok, item, deque} -> {item, deque}
:error -> raise ArgumentError, "deque is empty"
end
end
@doc ~S"""
Removes the item at the tail of the queue, returning it along with the
updated deque.
Returns `:error` if queue is empty.
"""
@spec pop_tail(t) :: {:ok, any, t} | :error
def pop_tail(deque) do
if deque.length == 0 do
:error
else
tail = tail(deque)
[{_, item}] = :ets.take(deque.table, tail)
new_deque = %{deque | length: deque.length - 1}
{:ok, item, new_deque}
end
end
@doc ~S"""
Removes the item at the tail of the queue, returning it along with the
updated deque.
Raises `ArgumentError` if queue is empty.
"""
@spec pop_tail!(t) :: {any, t}
def pop_tail!(deque) do
case pop_tail(deque) do
{:ok, item, deque} -> {item, deque}
:error -> raise ArgumentError, "deque is empty"
end
end
@doc ~S"""
Returns the item at the head of the queue, or `:error` if the queue
is empty.
"""
@spec peek_head(t) :: {:ok, any} | :error
def peek_head(deque) do
if deque.length == 0 do
:error
else
[{_, item}] = :ets.lookup(deque.table, deque.head)
{:ok, item}
end
end
@doc ~S"""
Returns the item at the head of the queue, or raises `ArgumentError`
if the queue is empty.
"""
@spec peek_head!(t) :: any
def peek_head!(deque) do
case peek_head(deque) do
{:ok, item} -> item
:error -> raise ArgumentError, "deque is empty"
end
end
@doc ~S"""
Returns the item at the tail of the queue, or `:error` if the queue
is empty.
"""
@spec peek_tail(t) :: {:ok, any} | :error
def peek_tail(deque) do
if deque.length == 0 do
:error
else
tail = tail(deque)
[{_, item}] = :ets.lookup(deque.table, tail)
{:ok, item}
end
end
@doc ~S"""
Returns the item at the tail of the queue, or raises `ArgumentError`
if the queue is empty.
"""
@spec peek_tail!(t) :: any
def peek_tail!(deque) do
case peek_tail(deque) do
{:ok, item} -> item
:error -> raise ArgumentError, "deque is empty"
end
end
@doc ~S"""
Returns the item at the given index, where index `0` is the head.
Returns `:error` if index is out of bounds.
"""
@spec at(t, non_neg_integer) :: {:ok, any} | :error
def at(deque, index) do
if deque.length > index do
[{_, item}] = :ets.lookup(deque.table, real_index(deque, index))
{:ok, item}
else
:error
end
end
@doc ~S"""
Returns the item at the given index, where index `0` is the head.
Raises `ArgumentError` if index is out of bounds.
"""
@spec at!(t, non_neg_integer) :: any
def at!(deque, index) do
case at(deque, index) do
{:ok, item} -> item
:error -> raise ArgumentError, "index #{index} out of bounds"
end
end
@doc ~S"""
Replaces the item at the given index, returning the updated deque.
Returns `:error` if index is out of bounds.
"""
@spec replace_at(t, non_neg_integer, any) :: {:ok, t} | :error
def replace_at(deque, index, item) do
if deque.length > index do
true = :ets.insert(deque.table, {real_index(deque, index), item})
{:ok, deque}
else
:error
end
end
@doc ~S"""
Replaces the item at the given index, returning the updated deque.
Raises `ArgumentError` if index is out of bounds.
"""
@spec replace_at!(t, non_neg_integer, any) :: t
def replace_at!(deque, index, item) do
case replace_at(deque, index, item) do
{:ok, deque} -> deque
:error -> raise ArgumentError, "index #{index} out of bounds"
end
end
@doc false
@spec new_head(t, integer) :: integer
def new_head(%{size: :infinity} = deque, increment) do
deque.head + increment
end
def new_head(deque, increment) do
rem(deque.size + deque.head + increment, deque.size)
end
defp tail(deque, decrement \\ 0)
defp tail(%{size: :infinity} = deque, decrement) do
deque.head - deque.length + 1 - decrement
end
defp tail(deque, decrement) do
rem(deque.size + deque.head - deque.length + 1 - decrement, deque.size)
end
defp real_index(deque, index) do
if deque.size == :infinity do
deque.head - index
else
rem(deque.size + deque.head - index, deque.size)
end
end
@behaviour Access
@impl Access
@doc false
def fetch(deque, index), do: at(deque, index)
@impl Access
@doc false
def get_and_update(deque, index, fun) do
case at(deque, index) do
{:ok, current} ->
case fun.(current) do
{get, update} ->
{:ok, deque} = replace_at(deque, index, update)
{get, deque}
:pop ->
{:ok, item, deque} = deque |> pop_head
{item, deque}
end
_error ->
raise ArgumentError, "index out of bounds"
end
end
@impl Access
@doc false
def pop(deque, index) do
cond do
index == 0 ->
{:ok, item, deque} = deque |> pop_head
{item, deque}
index == deque.length - 1 ->
{:ok, item, deque} = deque |> pop_tail
{item, deque}
:else ->
raise ArgumentError, "removing items not at head or tail is unsupported"
end
end
end
defimpl Collectable, for: EtsDeque do
def into(orig) do
{orig,
fn
deque, {:cont, item} ->
{:ok, deque} = deque |> EtsDeque.push_tail(item)
deque
deque, :done ->
deque
_, :halt ->
:ok
end}
end
end
defimpl Enumerable, for: EtsDeque do
def count(deque), do: {:ok, deque.length}
def member?(_deque, _item), do: {:error, __MODULE__}
def reduce(_deque, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(deque, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(deque, &1, fun)}
def reduce(%{length: 0}, {:cont, acc}, _fun), do: {:done, acc}
def reduce(deque, {:cont, acc}, fun) do
{:ok, head} = deque |> EtsDeque.peek_head()
new_head = EtsDeque.new_head(deque, -1)
deque = %{deque | head: new_head, length: deque.length - 1}
reduce(deque, fun.(head, acc), fun)
end
def slice(deque) do
{:ok, deque.length,
fn
_start, 0 ->
[]
start, len ->
Enum.reduce((start + len - 1)..start, [], fn index, acc ->
{:ok, item} = EtsDeque.at(deque, index)
[item | acc]
end)
end}
end
end