Current section

Files

Jump to
inplace lib adt linked_list.ex
Raw

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;
- `reclaim` :: boolean()
Handles if the element can be restored after removal (see delete_pointer/2).
`true` - element can not be restored, the place occupied by it can be reclaimed
(we will add the pointer to the removed element to the list of free pointers);
`false` - the element will be "hidden" by connecting prior and next elements of that element.
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);
Additionally, supports 'removal history', which could be used to "rewind" removals
in reverse order (see restore/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)
restore? = Keyword.get(opts, :restore)
if mode not in [@singly_linked_mode, @doubly_linked_mode] do
throw({:error, {:unknown_mode, mode}})
end
%{
capacity: size,
## mode (:singly_linked or :doubly_linked)
mode: mode,
## circular?
circular: circular?,
## Option to "undo" removals; see restore/1
restore: restore?,
## 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 restore? do
Map.put(state, :removed, Stack.new(size))
else
state
end
end)
end
## Initialization
##
defp default_opts() do
[
mode: @doubly_linked_mode,
circular: true,
restore: false,
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 `restore=true`, 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(%{restore: restore?, capacity: capacity, free: free} = list) do
capacity - Stack.size(free) - ((restore? && Stack.size(list.removed)) || 0)
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
apply_restore_strategy(list, pointer)
end
end
def pointer_deleted?(list, pointer) do
if list.restore do
empty?(list) ||
pointer != prev(list, next(list, pointer))
end
end
defp allocate(list) do
Stack.pop(list.free) || throw(:list_over_capacity)
end
defp apply_restore_strategy(list, pointer) do
if list.restore do
store_removal(list, pointer)
else
deallocate(list, pointer)
end
end
defp deallocate(list, pointer) do
Stack.push(list.free, pointer)
end
defp store_removal(list, pointer) do
Stack.push(list.removed, pointer)
end
def restore(%{restore: true, removed: removed} = list) do
case Stack.pop(removed) do
nil ->
false
restored_pointer ->
restore(list, restored_pointer)
end
end
def restore(_list) do
throw(:restore_disabled)
end
def restore(%{restore: true} = list, pointer) 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
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
:ok
end
def restore(_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