Current section
Files
Jump to
Current section
Files
lib/zipper/zipper.ex
defmodule RoseTree.Zipper do
alias RoseTree.Zipper
@moduledoc """
A zipper provides a mechanism for traversing a tree by focusing on
a given node and maintaining enough data to reconstruct the overall
tree from any given node.
Because most of the functions in `RoseTree.Zipper` can attempt to access data that
may not exist, e.g. an out-of-bounds index in a node's array of chlidren, the majority
of the functions return values with tagged tuples to let the user explicitly handle
success and error cases: `{:ok, {%RoseTree{}, []}} | {:error, {:rose_tree, error_message}}`
To make working with these values easier, the function `lift/2` takes one of these tagged
tuple (`either zipper | error`) values and a function. If the first argument is an error,
the error is passed through; if it is an `{:ok, zipper}` tuple, the function is applied to
the zipper. This lets you easily chain successive calls to tree manipulation functions.
"""
@type breadcrumb :: %{node: any(), index: integer(), other_children: [any()]}
@type t :: {RoseTree.t, [breadcrumb]}
@type either_zipper :: {:ok, Zipper.t} | {:error, tuple()}
@doc """
Build a zipper focusing on the current tree.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> Zipper.from_tree(tree)
{%RoseTree{node: :a, children: [
%RoseTree{node: :b, children: []},
%RoseTree{node: :c, children: [
%RoseTree{node: :d, children: []},
%RoseTree{node: :z, children: []}
]}
]}, []}
"""
@spec from_tree(RoseTree.t) :: Zipper.t
def from_tree(%RoseTree{} = tree), do: {tree, []}
@doc """
Extract the currently focused tree from a zipper.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> Zipper.descend({tree, []}, 0)
...> |> Zipper.lift(&Zipper.to_tree(&1))
%RoseTree{node: :b, children: []}
"""
@spec to_tree(Zipper.t) :: RoseTree.t
def to_tree({%RoseTree{} = tree, _crumbs} = _zipper), do: tree
@doc """
Move the zipper's focus to the child at the index.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> Zipper.descend({tree, []}, 0)
{:ok, {%RoseTree{node: :b, children: []}, [%{
node: :a,
index: 0,
other_children: [
%RoseTree{node: :c, children: [
%RoseTree{node: :d, children: []},
%RoseTree{node: :z, children: []}
]}
]
}]}}
"""
@spec descend(Zipper.t, integer()) :: {:ok, Zipper.t} | {:error, {:rose_tree, :no_children}}
def descend({%RoseTree{children: []}, _breadcrumbs} = _zipper, _index), do: {:error, {:rose_tree, :no_children}}
def descend({%RoseTree{} = tree, breadcrumbs} = _zipper, index) when is_list(breadcrumbs) and is_integer(index) do
with {elem, %RoseTree{node: node, children: updated_children}} <- RoseTree.pop_child_at(tree, index) do
new_breadcrumb = %{node: node, index: index, other_children: updated_children}
{:ok, {elem, [new_breadcrumb | breadcrumbs]}}
end
end
@doc """
Move up the tree to the parent of the node that the zipper is currently focused on.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> {:ok, descended} = Zipper.descend({tree, []}, 0)
...> Zipper.ascend(descended)
{:ok,
{%RoseTree{node: :a, children: [
%RoseTree{node: :b, children: []},
%RoseTree{node: :c, children: [
%RoseTree{node: :d, children: []},
%RoseTree{node: :z, children: []}
]}
]}, []}
}
"""
@spec ascend(Zipper.t) :: {:ok, Zipper.t} | {:error, {:rose_tree, :no_parent}}
def ascend({%RoseTree{}, []} = _zipper), do: {:error, {:rose_tree, :no_parent}}
def ascend({%RoseTree{} = tree, [%{index: idx, node: value, other_children: others} | crumbs]} = _zipper) do
siblings = List.insert_at(others, idx, tree)
{:ok, {%RoseTree{node: value, children: siblings}, crumbs}}
end
@doc """
Apply a function to the value of a tree under the focus of a zipper.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(1),
...> {:ok, d} <- RoseTree.new(11),
...> {:ok, z} <- RoseTree.new(12),
...> {:ok, c} <- RoseTree.new(10, [d, z]) do
...> RoseTree.new(0, [b, c])
...> end
...> {:ok, descended} = Zipper.descend({tree, []}, 0)
...> Zipper.modify(descended, fn(x) -> x * 5 end)
{%RoseTree{node: 5, children: []}, [%{
node: 0,
index: 0,
other_children: [
%RoseTree{node: 10, children: [
%RoseTree{node: 11, children: []},
%RoseTree{node: 12, children: []}
]}
]
}]}
"""
@spec modify(Zipper.t, (any() -> any())) :: Zipper.t
def modify({%RoseTree{node: node} = tree, crumbs} = _zipper, f) do
{%RoseTree{tree | node: f.(node)}, crumbs}
end
@doc """
Remove a subtree from a tree and move focus to the parent node.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(0)
...> |> Zipper.lift(&Zipper.prune(&1))
...> |> Zipper.to_tree()
%RoseTree{node: :a, children: [
%RoseTree{node: :c, children: [
%RoseTree{node: :d, children: []},
%RoseTree{node: :z, children: []}
]}
]}
"""
@spec prune(Zipper.t) :: Zipper.t
def prune({%RoseTree{}, [%{node: value, other_children: siblings} | crumbs]} = _zipper) do
{%RoseTree{node: value, children: siblings}, crumbs}
end
@doc """
Ascend from any node to the root of the tree.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(1)
...> |> Zipper.lift(&Zipper.descend(&1, 0))
...> |> Zipper.lift(&Zipper.to_root(&1))
%RoseTree{node: :a, children: [
%RoseTree{node: :b, children: []},
%RoseTree{node: :c, children: [
%RoseTree{node: :d, children: []},
%RoseTree{node: :z, children: []}
]}
]}
"""
@spec to_root(Zipper.t) :: RoseTree.t
def to_root({tree, []} = _zipper), do: tree
def to_root({_tree, _crumbs} = zipper), do: lift(ascend(zipper), &to_root/1)
@doc """
Move the zipper's focus to the node's first child.
If the node is a leaf (and thus has no children), returns {:error, {:rose_tree, :no_children}}.
Otherwise, returns {:ok, zipper}
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.first_child()
{:ok, {%RoseTree{node: :b, children: []}, [%{
node: :a,
index: 0,
other_children: [
%RoseTree{node: :c, children: [
%RoseTree{node: :d, children: []},
%RoseTree{node: :z, children: []}
]}
]
}]}}
"""
@spec first_child(Zipper.t) :: {:ok, Zipper.t} | {:error, {:rose_tree, :no_children}}
def first_child({%RoseTree{children: []}, _crumbs}), do: {:error, {:rose_tree, :bad_path}}
def first_child({%RoseTree{}, _crumbs} = zipper), do: descend(zipper, 0)
@doc """
Move the zipper's focus to the node's last child.
If the node is a leaf (and thus has no children), returns {:error, {:rose_tree, :no_children}}.
Otherwise, returns {:ok, zipper}
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.last_child()
{:ok, {
%RoseTree{node: :c, children: [
%RoseTree{node: :d, children: []},
%RoseTree{node: :z, children: []}
]},
[%{node: :a, index: 1, other_children: [%RoseTree{node: :b, children: []}]}]
}}
"""
@spec last_child(Zipper.t) :: {:ok, Zipper.t} | {:error, {:rose_tree, :no_children}}
def last_child({%RoseTree{children: []}, _crumbs}), do: {:error, {:rose_tree, :bad_path}}
def last_child({%RoseTree{} = tree, _crumbs} = zipper), do: descend(zipper, Enum.count(tree.children) - 1)
@doc """
Move the zipper's focus to the next sibling of the currently focused node.
## Examples
iex> {:ok, tree} = with {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [c])
...> end
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(0)
...> |> Zipper.lift(&Zipper.descend(&1, 0))
...> |> Zipper.lift(&Zipper.next_sibling/1)
{:ok, {
%RoseTree{children: [], node: :z}, [
%{index: 1, node: :c, other_children: [%RoseTree{children: [], node: :d}]},
%{index: 0, node: :a, other_children: []}
]
}}
iex> {:ok, tree} = with {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [c])
...> end
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(0)
...> |> Zipper.lift(&Zipper.descend(&1, 1))
...> |> Zipper.lift(&Zipper.next_sibling/1)
{:error, {:rose_tree, :no_next_sibling}}
"""
@spec next_sibling(Zipper.t) :: {:ok, Zipper.t} | {:error, {:rose_tree, :no_siblings}} | {:error, {:rose_tree, :no_next_sibling}}
def next_sibling({%RoseTree{}, []}), do: {:error, {:rose_tree, :no_siblings}}
def next_sibling({%RoseTree{}, [%{index: index, other_children: children} | _t]} = zipper) do
at_last_child = (index + 1) > length(children)
if at_last_child do
{:error, {:rose_tree, :no_next_sibling}}
else
zipper
|> ascend()
|> lift(&descend(&1, index + 1))
end
end
@doc """
Move the zipper's focus to the previous sibling of the currently focused node.
## Examples
iex> {:ok, tree} = with {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [c])
...> end
...> d_focus = tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(0)
...> |> Zipper.lift(&Zipper.descend(&1, 1))
...> |> Zipper.lift(&Zipper.previous_sibling/1)
{:ok, {
%RoseTree{children: [], node: :d}, [
%{index: 0, node: :c, other_children: [%RoseTree{children: [], node: :z}]},
%{index: 0, node: :a, other_children: []}
]
}}
iex> {:ok, tree} = with {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [c])
...> end
...> d_focus = tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(0)
...> |> Zipper.lift(&Zipper.descend(&1, 0))
...> |> Zipper.lift(&Zipper.previous_sibling/1)
{:error, {:rose_tree, :no_previous_sibling}}
"""
@spec previous_sibling(Zipper.t) :: {:ok, Zipper.t} | {:error, {:rose_tree, :no_siblings}} | {:error, {:rose_tree, :no_previous_sibling}}
def previous_sibling({%RoseTree{}, []}), do: {:error, {:rose_tree, :no_siblings}}
def previous_sibling({%RoseTree{}, [%{index: index} | _t]} = zipper) do
at_first_child = (index - 1) < 0
if at_first_child do
{:error, {:rose_tree, :no_previous_sibling}}
else
zipper
|> ascend()
|> Zipper.lift(&descend(&1, index - 1))
end
end
@doc """
Move the zipper's focus to the node's first child that matches a predicate.
## Examples
iex> {:ok, tree} = with {:ok, d} <- RoseTree.new(15),
...> {:ok, z} <- RoseTree.new(5),
...> {:ok, c} <- RoseTree.new(1, [d, z]) do
...> RoseTree.new(:a, [c])
...> end
...> d_focus = tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(0)
...> |> Zipper.lift(&Zipper.find_child(&1, fn(child) -> child.node > 10 end))
{:ok, {
%RoseTree{children: [], node: 15}, [
%{index: 0, node: 1, other_children: [%RoseTree{children: [], node: 5}]},
%{index: 0, node: :a, other_children: []}
]
}}
iex> {:ok, tree} = with {:ok, d} <- RoseTree.new(15),
...> {:ok, z} <- RoseTree.new(5),
...> {:ok, c} <- RoseTree.new(1, [d, z]) do
...> RoseTree.new(:a, [c])
...> end
...> d_focus = tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(0)
...> |> Zipper.lift(&Zipper.find_child(&1, fn(child) -> length(child.children) > 5 end))
{:error, {:rose_tree, :no_child_match}}
"""
@spec find_child(Zipper.t, (any() -> any())) :: {:ok, Zipper.t} | {:error, {:rose_tree, :no_child_match}}
def find_child({%RoseTree{children: children}, _crumbs} = zipper, predicate) when is_function(predicate) do
matching_index = Enum.find_index(children, predicate)
if matching_index == nil do
{:error, {:rose_tree, :no_child_match}}
else
descend(zipper, matching_index)
end
end
@doc """
Lift a function expecting a zipper as an argument to one that can handle either
{:ok, zipper} | {:error, error}.
If the first argument is an :error tuple, the error is passed through.
Otherwise, the function is applied to the zipper in the :ok tuple.
## Examples
iex> {:ok, tree} = with {:ok, b} <- RoseTree.new(:b),
...> {:ok, d} <- RoseTree.new(:d),
...> {:ok, z} <- RoseTree.new(:z),
...> {:ok, c} <- RoseTree.new(:c, [d, z]) do
...> RoseTree.new(:a, [b, c])
...> end
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(1)
...> |> Zipper.lift(&Zipper.descend(&1, 0))
...> |> Zipper.lift(&Zipper.to_tree(&1))
%RoseTree{node: :d, children: []}
...> tree
...> |> Zipper.from_tree()
...> |> Zipper.descend(1)
...> |> Zipper.lift(&Zipper.descend(&1, 0))
...> |> Zipper.lift(&Zipper.descend(&1, 1))
{:error, {:rose_tree, :bad_path}}
"""
@spec lift(either_zipper, (any() -> any())) :: either_zipper
def lift({:ok, zipper} = _either_zipper_error, f), do: f.(zipper)
def lift({:error, _} = either_zipper_error, _f), do: either_zipper_error
end