Current section
Files
Jump to
Current section
Files
lib/zippex.ex
defmodule Zippex do
@moduledoc """
A Zipper is a representation of an aggregate data structure which
allows it to be traversed and updated arbitrarily. This module
implements tree-like semantics for traversing a data structure.
## Focus
The current node of the zipper, also known as the focus node, can be
retrieved by calling the `focus/1` function. The following functions
provide other information relating to the focus node:
* `lefts/1` - returns the left siblings of the focus node
* `rights/1` - returns the rights siblings of the focus node
* `path/1` - returns the path to the focus node from the root
## Traversal
The focus can be moved using the following functions:
* `head/1` - moves to the root node
* `down/1` - moves to the first child of the focus node
* `up/1` - moves to the parent of the focus node
* `left/1` - moves to the left sibling of the focus node
* `leftmost/1` - moves to the leftmost sibling of the focus node
* `right/1` - moves to the right sibling of the focus node
* `rightmost/1` - moves to the rightmost sibling of the focus node
* `next/1` - moves to the next node in a depth-first traversal
* `prev/1` - moves to the previous node in a depth-first traversal
## Enumeration
`Zippex` implements the `Enumerable` protocol, which allows it's values
to be enumerated in a depth-first traversal.
## Updates
The focus node can be modified using the functions `edit/2` or `edit/3`.
It can be removed, along with it's children, using the `remove/1` function,
after which the focus is moved to the previous node in a depth-first
traversal.
"""
import Kernel, except: [node: 1]
alias Zippex.Context
alias Zippex.Meta
defstruct [:spec, :node, :ctx]
@type t :: %__MODULE__{spec: Meta.t(), node: element, ctx: Context.t() | :end}
@type edit_fun :: (element -> element)
@type edit_with_args_fun :: (element, args -> element)
@type element :: any
@type args :: any
@doc """
Returns a new Zipper for a given `node` element.
`is_branch_fun` receives a node and returns `true` if it is a branch, or
`false` otherwise.
`children_fun` receives a node (which is a branch) and returns a list of
it's child nodes.
`make_node_fun` receives a parent node and a list of child nodes and
returns a new node.
"""
@spec new(Meta.is_branch_fun(), Meta.children_fun(), Meta.make_node_fun(), element) :: t
def new(is_branch, children, make_node, root) do
spec = Meta.new(is_branch, children, make_node)
%Zippex{
node: root,
spec: spec,
ctx: %Context{}
}
end
@doc """
Returns the focus node of a zipper.
"""
@spec focus(t) :: element
def focus(zipper)
def focus(%Zippex{node: n}), do: n
@doc """
Returns the root node of the zipper.
"""
@spec root(t) :: element
def root(%Zippex{} = zipper) do
zipper |> head() |> focus()
end
@doc """
Returns the path to the focus node.
"""
@spec path(t) :: list(element)
def path(%Zippex{ctx: ctx}) do
Context.path(ctx, [])
end
@doc """
Returns the left siblings of the focus node.
"""
@spec lefts(t) :: list(element)
def lefts(%Zippex{ctx: %{left: ls}}), do: ls
@doc """
Returns the right siblings of the focus node.
"""
@spec rights(t) :: list(element)
def rights(%Zippex{ctx: %{right: rs}}), do: rs
@doc """
Moves to the head of the zipper.
"""
@spec head(t) :: element
def head(%Zippex{} = zipper) do
case up(zipper) do
nil -> leftmost(zipper)
z -> head(z)
end
end
@doc """
Moves to the left sibling of the focus node.
Returns the updated zipper, or `nil` if the focus node has no left sibling.
"""
@spec left(t) :: t | nil
def left(%Zippex{node: n, ctx: ctx} = zipper) do
case ctx do
%{left: []} ->
nil
%{left: [prev | left], right: right} ->
ctx = %{ctx | left: left, right: [n | right]}
%{zipper | node: prev, ctx: ctx}
end
end
@doc """
Moves to the leftmost sibling of the focus node.
Returns the updated zipper.
"""
@spec leftmost(t) :: t
def leftmost(%Zippex{node: n, ctx: ctx} = zipper) do
case ctx do
%{left: []} ->
zipper
%{left: ls, right: rs} ->
[leftmost | right] = Enum.reduce(ls, [n | rs], &[&1 | &2])
ctx = %{ctx | left: [], right: right}
%{zipper | node: leftmost, ctx: ctx}
end
end
@doc """
Moves to the right sibling of the focus node.
Returns the updated zipper, or `nil` if the focus node has no right sibling.
"""
@spec right(t) :: t | nil
def right(%Zippex{node: n, ctx: ctx} = zipper) do
case ctx do
%{right: []} ->
nil
%{left: left, right: [next | right]} ->
ctx = %{ctx | left: [n | left], right: right}
%{zipper | node: next, ctx: ctx}
end
end
@doc """
Moves to the rightmost sibling of the focus node.
Returns the updated zipper.
"""
@spec rightmost(t) :: t
def rightmost(%Zippex{node: n, ctx: ctx} = zipper) do
case ctx do
%{right: []} ->
zipper
%{left: ls, right: rs} ->
[rightmost | left] = Enum.reduce(rs, [n | ls], &[&1 | &2])
ctx = %{ctx | left: left, right: []}
%{zipper | node: rightmost, ctx: ctx}
end
end
@doc """
Moves to the parent of the focus node.
Returns the updated zipper, or `nil` if the focus node has no parent.
"""
@spec up(t) :: t | nil
def up(%Zippex{node: n, ctx: ctx, spec: spec} = zipper) do
case ctx do
%{parent: nil} ->
nil
%{parent: parent, ctx: parent_ctx, dirty: false} ->
%{zipper | node: parent, ctx: parent_ctx}
%{left: left, right: right, parent: parent, ctx: parent_ctx, dirty: true} ->
children = Enum.reverse(left) ++ [n | right]
parent = Meta.make_node(spec, parent, children)
%{zipper | node: parent, ctx: %{parent_ctx | dirty: true}}
end
end
@doc """
Moves to the first child of the focus node.
Returns the updated zipper, or `nil` if the focus node has no children.
"""
@spec down(t) :: t | nil
def down(%Zippex{ctx: parent_ctx, node: parent, spec: spec} = zipper) do
if Meta.is_branch(spec, parent) do
case Meta.children(spec, parent) do
[child | right] ->
ctx = %Context{left: [], right: right, parent: parent, ctx: parent_ctx}
%{zipper | node: child, ctx: ctx}
_ ->
nil
end
end
end
@doc """
Moves to the next node of the focus node in a depth-first traversal.
"""
@spec next(t) :: t
def next(%Zippex{spec: spec, node: n} = zipper) do
if Meta.is_branch(spec, n) do
down(zipper)
else
case right(zipper) do
nil -> next_recur(zipper)
right -> right
end
end
end
@spec next_recur(t) :: t
defp next_recur(%Zippex{} = zipper) do
case up(zipper) do
nil ->
%{zipper | ctx: :end}
z ->
case right(z) do
nil -> next_recur(z)
right -> right
end
end
end
@doc """
Moves to the previous node of the focus node in a depth-first traversal.
"""
@spec prev(t) :: t
def prev(%Zippex{ctx: ctx} = zipper) do
case ctx do
%{left: []} -> up(zipper)
_ -> prev_recur(zipper)
end
end
@spec prev_recur(t) :: t
defp prev_recur(%Zippex{} = zipper) do
case down(zipper) do
nil ->
zipper
z ->
z |> rightmost() |> prev_recur()
end
end
@doc """
Removes the focus node, moving the focus to the node that would have
preceded it in a depth-first traversal.
"""
@spec remove(t) :: t
def remove(%Zippex{ctx: ctx, spec: spec} = zipper) do
case ctx do
%{ctx: nil} ->
raise(ArgumentError, "can't remove root")
%{left: [], right: right, parent: parent, ctx: parent_ctx} ->
parent_ctx = %{parent_ctx | dirty: true}
%{zipper | node: Meta.make_node(spec, parent, right), ctx: parent_ctx}
%{left: [l | ls]} ->
ctx = %{ctx | left: ls, dirty: true}
%{zipper | node: l, ctx: ctx} |> remove_prev()
end
end
@spec remove_prev(t) :: t
defp remove_prev(%Zippex{} = zipper) do
case down(zipper) do
nil -> zipper
z -> z |> rightmost() |> remove_prev()
end
end
@doc """
Modifies the focus node by applying a function to it.
"""
@spec edit(t, edit_fun) :: t
def edit(%Zippex{node: n, ctx: ctx} = zipper, fun) do
%{zipper | node: fun.(n), ctx: %{ctx | dirty: true}}
end
@doc """
Modifies the focus node by applying a function to it.
"""
@spec edit(t, edit_with_args_fun, args) :: t
def edit(%Zippex{node: n, ctx: ctx} = zipper, fun, args) do
%{zipper | node: fun.(n, args), ctx: %{ctx | dirty: true}}
end
defimpl Enumerable do
@impl true
def reduce(zipper, acc, fun)
def reduce(_zipper, {:halt, acc}, _fun), do: {:halted, acc}
def reduce(zipper, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(zipper, &1, fun)}
def reduce(%Zippex{ctx: ctx, node: n} = zipper, {:cont, acc}, fun) do
case ctx do
:end ->
{:done, acc}
_ ->
zipper
|> Zippex.next()
|> reduce(fun.(n, acc), fun)
end
end
@impl true
def count(_zipper), do: {:error, __MODULE__}
@impl true
def member?(_zipper, _element), do: {:error, __MODULE__}
@impl true
def slice(_zipper), do: {:error, __MODULE__}
end
end