Packages
inplace
0.3.2
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Mutable data structures
Current section
Files
Jump to
Current section
Files
lib/adt/linked_list.ex
defmodule InPlace.LinkedList do
@moduledoc """
[Singly linked list](https://en.wikipedia.org/wiki/Linked_list#Singly_linked_list)
The data entries are stored as integers.
They will be interpreted (as references) by the calling application.
Note: indices are 1-based.
Options:
- `mode` :: :singly_linked | :doubly_linked
- `circular` :: boolean()
optional, `false` by default;
- `deletion` :: :reclaim | :hide | :rewind
Handles if the element pointer could be restored later (see delete_pointer/2), and how
to restore it:
`:reclaim` (default) - element can not be restored, the pointer will be put back to the `allocation` pool;
`:hide` - element can be restored by re-linking with it's former `left` and `right` neighbors;
The element stays in the list, but can not be reached
except by directly addressed by it's pointer. The element can be put back to it's position
by reconnecting previously prior and next elements back to that element (see hide/2 and restore/2);
`:rewind` - if specified, the special stack of removed element pointers is maintained.
The effect of removal will be the same as for :hide, except that
The elements can be restored in the reverse order of their removal by using `rewind/1`.
- `:mapper_fun` # maps data entries to application data
optional, &Function.identity/1 by default
"""
alias InPlace.{Array, Stack}
## List terminator
@terminator 0
@singly_linked_mode :singly_linked
@doubly_linked_mode :doubly_linked
def new(values_or_size, opts \\ [])
def new(values, opts) when is_list(values) do
new(max(length(values), Keyword.get(opts, :capacity, 0)), opts)
|> tap(fn ll -> Enum.each(values, fn v -> append(ll, v) end) end)
end
def new(size, opts) when is_integer(size) and size > 0 do
opts = Keyword.merge(default_opts(), opts)
mode = Keyword.get(opts, :mode)
circular? = Keyword.get(opts, :circular)
deletion_mode = Keyword.get(opts, :deletion)
if mode not in [@singly_linked_mode, @doubly_linked_mode] do
throw({:error, {:unknown_mode, mode}})
end
%{
capacity: size,
size: Array.new(1, 0),
## mode (:singly_linked or :doubly_linked)
mode: mode,
## circular?
circular: circular?,
## Option to handle removals
deletion: deletion_mode,
## holds the pointer to the first element of the list
handle: init_handle(mode),
# pointers (links) to the next element
next: init_links(size),
# pointer (links) to the previous element
prev: init_links(size),
# allocation pool
free: init_free(size),
# references to the data
refs: :atomics.new(size, signed: true),
# mapper `reference -> data`
mapper_fun: Keyword.get(opts, :mapper_fun)
}
|> then(fn state ->
if deletion_mode == :rewind do
Map.put(state, :removed, Stack.new(size))
else
state
end
end)
end
## Initialization
##
defp default_opts() do
[
mode: @doubly_linked_mode,
circular: true,
deletion: :reclaim,
mapper_fun: &Function.identity/1
]
end
## Initialize handle.
## Holds pointer to first element
defp init_handle(_mode) do
Array.new(1, @terminator)
end
## Allocate links (pointers to the next element)
defp init_links(size) do
:atomics.new(size, signed: false)
end
## The stack for tracking 'free' indices
## They can be reused after the element is removed from linked list
## (unless `deletion in [:hide, :rewind]`, which disables reuse).
defp init_free(size) when is_integer(size) do
ref = Stack.new(size)
Enum.each(size..1//-1, fn idx -> Stack.push(ref, idx) end)
ref
end
def head(%{handle: handle} = _list) do
Array.get(handle, 1)
end
def tail(list) do
prev(list, head(list))
end
def empty?(list) do
head(list) == @terminator
end
def size(list) do
Array.get(list.size, 1)
end
def inc_size(list, delta) do
Array.update(list.size, 1, fn s -> s + delta end)
end
def available(list) do
list.capacity - size(list)
end
def next(_list, @terminator) do
@terminator
end
def next(list, pointer) do
Array.get(list.next, pointer)
end
def prev(_list, @terminator) do
@terminator
end
def prev(list, pointer) do
Array.get(list.prev, pointer)
end
def append(list, data) when is_integer(data) do
last_pointer = tail(list)
add_pointer_after(list, last_pointer, data)
:ok
end
defp add_pointer_after(list, pointer, data) do
allocated = allocate(list)
set_data(list, allocated, data)
case next(list, pointer) do
@terminator ->
## Empty list, first pointer to set
wire(list, allocated, allocated)
set_head(list, allocated)
next_pointer ->
wire(list, allocated, next_pointer)
wire(list, pointer, allocated)
end
allocated
end
def add_first(list, data) do
new_pointer = add_pointer_after(list, tail(list), data)
set_head(list, new_pointer)
end
def delete_pointer(list, pointer) do
if pointer_deleted?(list, pointer) do
false
else
next_pointer = next(list, pointer)
prev_pointer = prev(list, pointer)
set_prev(list, next_pointer, prev_pointer)
set_next(list, prev_pointer, next_pointer)
if pointer == head(list) do
set_head(list, (size(list) == 1 && @terminator) || next_pointer)
end
dispose_or_hide(list, pointer)
end
end
def pointer_deleted?(list, pointer) do
empty?(list) ||
pointer != prev(list, next(list, pointer))
end
defp allocate(list) do
if size(list) == list.capacity do
throw(:list_over_capacity)
else
inc_size(list, 1)
Stack.pop(list.free)
end
end
defp dispose_or_hide(%{deletion: deletion_mode} = list, pointer) do
inc_size(list, -1)
case deletion_mode do
:hide ->
:ok
:reclaim ->
dispose(list, pointer)
:rewind ->
hide(list, pointer)
end
end
defp dispose(list, pointer) do
Stack.push(list.free, pointer)
end
defp hide(list, pointer) do
Stack.push(list.removed, pointer)
end
def rewind(%{deletion: :rewind, removed: removed} = list) do
case Stack.pop(removed) do
nil ->
false
restored_pointer ->
restore_pointer(list, restored_pointer)
{:restored, restored_pointer}
end
end
def rewind(_list) do
throw(:rewind_disabled)
end
def restore_pointer(%{deletion: deletion_mode} = list, pointer) when deletion_mode in [:hide, :rewind] do
next_pointer = next(list, pointer)
prev_pointer = prev(list, pointer)
set_prev(list, next_pointer, pointer)
set_next(list, prev_pointer, pointer)
## Special cases:
## - next_pointer for restored pointer
## points to a current head;
## - the list is empty
## - there is no more pointers in `removed` stack
cond do
deletion_mode == :rewind && Stack.empty?(list.removed) ->
set_head(list, 1)
head(list) in [@terminator, next_pointer] ->
## We replace head with the restored pointer
set_head(list, pointer)
true ->
:ok
end
inc_size(list, 1)
:ok
end
def restore_pointer(_list, _pointer) do
throw(:restore_disabled)
end
defp set_head(list, pointer) do
Array.put(list.handle, 1, pointer)
end
defp set_next(_list, _pointer, @terminator) do
throw(:setting_invalid_next_pointer)
end
defp set_next(list, pointer, next_pointer) do
Array.put(list.next, pointer, next_pointer)
end
defp set_prev(_list, _pointer, @terminator) do
throw(:setting_invalid_prev_pointer)
end
defp set_prev(list, pointer, prev_pointer) do
Array.put(list.prev, pointer, prev_pointer)
end
def set_data(list, pointer, data) do
Array.put(list.refs, pointer, data)
end
def data(%{refs: refs, mapper_fun: mapper} = _list, pointer) do
mapper.(Array.get(refs, pointer))
end
############
## Iteration
############
def to_list(list) do
reduce(list, []) |> Enum.reverse()
end
def reduce(list, initial_value, reducer \\ nil) do
iterate(list, reducer || default_reducer(list), initial_value: initial_value)
end
defp default_reducer(list) do
fn p, acc ->
[data(list, p) | acc]
end
end
def iterate(list, action, opts \\ [])
def iterate(list, action, opts) do
start = Keyword.get(opts, :start, head(list))
forward? = Keyword.get(opts, :forward, true)
initial_value = Keyword.get(opts, :initial_value, [])
stop_on =
Keyword.get(opts, :stop_on, fn next ->
last_pointer = (list.circular && start) || @terminator
next == last_pointer
end)
## If action is of arity 1, it's a "side-effect" function.
## The argument is a pointer.
## For instance, it might conditionally delete some entries,
## etc.
## The result of the action call would be ignored.
## If action is of arity 2, it's a "reducer" function
## The arguments are : pointer and accumulated value
## If {:halt, term()} is returned, the iteration is halted with term()
## If {:cont, term()} or term() is returned, the iteration continues.
##
cond do
is_function(action, 1) ->
iterate_impl(list, start, stop_on, action, forward?)
is_function(action, 2) ->
iterate_impl(list, start, stop_on, action, forward?, initial_value)
true ->
throw({:error, :action_invalid_arity})
end
end
## "Reducer" iteration
defp iterate_impl(_list, @terminator, _stop_on, action, acc) when is_function(action, 2) do
acc
end
defp iterate_impl(list, current_pointer, stop_on, action, acc) when is_function(action, 2) do
case action.(current_pointer, acc) do
{:halt, new_acc} ->
new_acc
result ->
new_acc =
case result do
{:cont, r} ->
r
r ->
r
end
next_p = next(list, current_pointer)
if next_p == current_pointer || stop_on.(next_p) do
new_acc
else
iterate_impl(list, next_p, stop_on, action, new_acc)
end
end
end
## Iteration with side-effects
defp iterate_impl(_list, @terminator, _stop_on, action, _forward?)
when is_function(action, 1) do
:ok
end
defp iterate_impl(list, current_pointer, stop_on, action, forward?)
when is_function(action, 1) do
case action.(current_pointer) do
:halt ->
:ok
_ ->
next_p = (forward? && next(list, current_pointer)) || prev(list, current_pointer)
if next_p == current_pointer || stop_on.(next_p) do
:ok
else
iterate_impl(list, next_p, stop_on, action, forward?)
end
end
end
## "Reducer" iteration
defp iterate_impl(_list, @terminator, _stop_on, action, _forward?, acc)
when is_function(action, 2) do
acc
end
defp iterate_impl(list, current_pointer, stop_on, action, forward?, acc)
when is_function(action, 2) do
case action.(current_pointer, acc) do
{:halt, new_acc} ->
new_acc
result ->
new_acc =
case result do
{:cont, r} ->
r
r ->
r
end
next_p = (forward? && next(list, current_pointer)) || prev(list, current_pointer)
if stop_on.(next_p) do
new_acc
else
iterate_impl(list, next_p, stop_on, action, forward?, new_acc)
end
end
end
#################
## Positional API
#################
@doc """
Get element at `position` (1-based)
"""
def get(list, position) when is_integer(position) and position > 0 do
if position <= size(list) do
iterate(
list,
fn pointer, idx_acc ->
if idx_acc == position do
{:halt, data(list, pointer)}
else
{:cont, idx_acc + 1}
end
end,
initial_value: 1
)
end
end
def get(_list, _position) do
nil
end
@doc """
Insert `data` element at `position` (1-based)
"""
def insert(list, position, data) when is_integer(position) and is_integer(data) do
cond do
position == 1 ->
add_first(list, data)
position == size(list) + 1 ->
append(list, data)
position <= size(list) ->
iterate(
list,
fn pointer, idx_acc ->
if idx_acc == position - 1 do
{:halt, add_pointer_after(list, pointer, data)}
else
{:cont, idx_acc + 1}
end
end,
initial_value: 1
)
true ->
false
end
end
@doc """
Delete element at `position` (1-based)
"""
def delete(list, position) when is_integer(position) and position > 0 do
cond do
position <= size(list) ->
iterate(
list,
fn pointer, idx_acc ->
if idx_acc == position do
{:halt, delete_pointer(list, pointer)}
else
{:cont, idx_acc + 1}
end
end,
initial_value: 1
)
true ->
false
end
end
#######################################
### Sublists (circuits, partitions...)
#######################################
@doc """
This will create a circuit out of the list of pointers.
!!!!! Hazard warning !!!!!
If you want to avoid infinite loops
while iterating over the list that contains circuits,
make sure that you start the iteration within the circuit
(using :start option fir iterate/2).
For instance
```elixir
import InPlace.LinkedList
ll = new(Enum.to_list(1..10))
circuit(ll, [2, 4, 6])
iterate, start: 1, action: fn p -> IO.inspect(p) end
```
will loop indefinitely, as the iteration will be trapped in `2 -> 4 -> 6` circuit
once entering it from `1` pointer.
"""
def circuit(_list, []) do
:ok
end
def circuit(list, [single]) do
wire(list, single, single)
end
def circuit(list, [first, second | rest] = _pointers) do
wire(list, first, second)
last = circuit_impl(list, second, rest)
## Short the circuit
wire(list, last, first)
end
defp circuit_impl(_list, last, []) do
last
end
defp circuit_impl(list, current, [next | rest]) do
wire(list, current, next)
circuit_impl(list, next, rest)
end
defp wire(list, first, second) do
set_next(list, first, second)
set_prev(list, second, first)
end
end