Packages

Traverse is a toolset to walk arbitrary Elixir Datastructures in a functional way.

Current section

Files

Jump to
traverse lib traverse.ex
Raw

lib/traverse.ex

defmodule Traverse do
use Traverse.Types
use Traverse.Macros
defmodule Cut do
@moduledoc """
A wrapper around the accumulator value of the traversal function, which will
avoid recursive decent from this node on.
"""
defstruct acc: "boxed accumulator"
def me?(%__MODULE__{}), do: true
def me?(_), do: false
end
defmodule Ignore do
@moduledoc """
When a transformer function returns this value the transformation of the
containing data structure will not contain it, in case the containing data structure is
a map the key is omitted in the transformation.
iex> Traverse.map([1, 2, %{a: 1}, {1, 2}], fn _ -> Traverse.Ignore end)
[%{}, {}]
"""
@doc """
Lackmus to decide if an argument is to be ignored, or, in other words, is me.
"""
def me?(__MODULE__), do: true
def me?(_), do: false
end
@moduledoc """
# Traverse is a toolset to walk arbitrary Elixir Datastructures.
## Walking The Whole Structure
`walk` visits all substructures down to atomic elements.
iex(0)> ds = [:a, {:b, 1, 2}, [:c, 3, 4, 5]]
...(0)> collector = fn ele, acc when is_atom(ele) or is_number(ele) -> [ele|acc]
...(0)> _, acc -> acc end
...(0)> Traverse.walk(ds, [], collector)
[5, 4, 3, :c, 2, 1, :b, :a]
But substructures are of course visited too:
iex(1)> ds = [:a, {:b, 1, 2}, [:c, 3, 4, 5]]
...(1)> collector = fn ele, acc -> [ele|acc] end
...(1)> Traverse.walk(ds, [], collector) |> Enum.reverse
[[:a, {:b, 1, 2}, [:c, 3, 4, 5]], :a, {:b, 1, 2}, :b, 1, 2, [:c, 3, 4, 5], :c, 3, 4, 5]
This example shows that the default visiting strategy is depth first
and prewalk, in other words, the algorithm is descending the leftmost path, calling the visiting
function _before_ descending.
We can however instruct it to use a postwalk strategy as follows
iex(2)> ds = [:a, [:c, 3]]
...(2)> collector = fn ele, acc -> [ele|acc] end
...(2)> Traverse.walk(ds, [], collector, postwalk: true)
[[:a, [:c, 3]], [:c, 3], 3, :c, :a]
For the time being the depth first strategy cannot be changed.
### Cutting substructures off
Let us say that we do not want to traverse certain, subtrees, as in the following example
in which `%TraverseCut{}` is used to cut subtrees if which the key is `:ignore`:
iex(3)> ds = %{a: %{ignore: [1, 3, 4]}, b: 10, c: %{e: 20, d: [f: 30, ignore: 1000]}}
...(3)> collector = fn
...(3)> ele, acc when is_number ele -> acc + ele
...(3)> {:ignore, _}, acc -> %Traverse.Cut{acc: acc}
...(3)> _ , acc -> acc end
...(3)> Traverse.walk(ds, 0, collector)
60
In postwalk scenarii this does not make any sense of course
iex(4)> ds = %{a: [1, 2], ignore: [3, 4]}
...(4)> collector = fn
...(4)> ele, acc when is_number ele -> acc + ele
...(4)> {:ignore, _}, acc -> %Traverse.Cut{acc: acc}
...(4)> _ , acc -> acc end
...(4)> Traverse.walk(ds, 0, collector, postwalk: true)
%Traverse.Cut{acc: 10}
Therefore postwalk does not even unbox the Cut struct which might lead to errors we used
this example only to show that when the cut is applied the accumulator has already added
the values from the substructure.
### Partial functions
The astuce reader might have noticed that most of our collector functions above
had a default clause like that:
_, acc -> acc end
It is tempting to complete partial collector functions this way automatically.
However this has major downsides:
- application errors are masked by the rescue clause
- stack traces are harder to read
- runtime increases
That said, especially in iex sessions it might be useful to be able doing this.
Enter the bang version: `walk!`
iex(5)> ds = %{a: 1, b: 2}
...(5)> Traverse.walk!(ds, 0, fn ele, acc when is_number(ele) -> ele + acc end)
3
Just to show the difference with the unbanged version of `walk`:
iex(6)> ds = %{a: 1, b: 2}
...(6)> try do
...(6)> Traverse.walk(ds, 0, fn ele, acc when is_number(ele) -> ele + acc end)
...(6)> rescue
...(6)> FunctionClauseError -> :rescued
...(6)> end
:rescued
The bang version can also be used with `postwalk: true` of course.
iex(7)> ds = %{a: 1, b: 2}
...(7)> Traverse.walk!(ds, 0, fn ele, acc when is_number(ele) -> ele + acc end, postwalk: true)
3
## Mapping
While walking implements the most general way to traverse common data structures it does not preserve
the structure of the walked data structure by itself.
Mapping will descend the data structure and copy it, but apply the mapper function **only** to leaves.
Therefore it is sufficient to define the __mapper__ function for the type of leave values only.
However, while map keys are not leaves, keyword lists are just list of tuples and as such the __keys__ are
considered leaves too.
iex(8)> ds = [ a: 1, b: %{ c: [1, 2], d: [e: 100, f: 200] } ]
...(8)> mapper = fn x when is_number(x) -> x + 1
...(8)> x when is_atom(x) -> to_string(x)
...(8)> x -> x end
...(8)> Traverse.map(ds, mapper)
[{"a", 2}, {"b", %{c: [2, 3], d: [{"e", 101}, {"f", 201}]}}]
### Partial functions
As seen above it might again be convenient to automatically replace undefined parts
of the mapper function with the identity function, and the banged version, `map!` is
just doing that
iex(9)> ds = [ a: 1, b: %{ c: [1, 2], d: [e: 100, f: 200] } ]
...(9)> Traverse.map!(ds, fn x when is_number(x) -> x + 1 end)
[ a: 2, b: %{ c: [2, 3], d: [e: 101, f: 201] } ]
### Structural Preserving Traversal
While `walk` is a general way of traversing data structures, and `map` is a convenient way
of applying changes to leaves only, `mapall` is a compromise between both.
If `walk` just copies the structure of the data structure into its accumulator, `mapall` does
this automatically by applying the _mapper_ function to the copied data structure.
This is done via a trick which might cause some confiusion in debugging, while `mapall` does not
complete the definition of the __mapper__ function like `walk!` and `map!` it still rescues
`FunctionClauseError` when applying __mapper__ to __inner__ nodes.
Therefore
iex(10)> ds = [ %{a: 1, b: 2}, [3]]
...(10)> mapper = fn x when is_number(x) -> x + 1 end
...(10)> Traverse.mapall(ds, mapper)
[%{a: 2, b: 3}, [4]]
But
iex(11)> ds = [ %{a: 1, b: 2}, [3]]
...(11)> try do
...(11)> Traverse.mapall(ds, &(&1 + 1))
...(11)> rescue
...(11)> _ -> :rescued
...(11)> end
:rescued
And
iex(12)> ds = [ %{a: 1, b: :hello}, [3]]
...(12)> mapper = fn x when is_number(x) -> x + 1 end
...(12)> try do
...(12)> Traverse.mapall(ds, mapper)
...(12)> rescue
...(12)> _ -> :rescued
...(12)> end
:rescued
## Filtering
Filtering could be implemented by `mapall` and a traversal function that returns either
an `Ignore` value or the input paramater. Let us demonstrate with the following example
iex(13)> ds = [ %{a: 1}, [2, 3] ]
...(13)> odd_list_elements = fn x when is_number(x) -> if rem(x, 2) == 1, do: x, else: Traverse.Ignore
...(13)> x when is_list(x) -> x
...(13)> _ -> Traverse.Ignore end
...(13)> Traverse.mapall(ds, odd_list_elements)
[ [3] ]
`Traverse` filter removes lots of the boilerplate
iex(14)> ds = [ %{a: 1}, [2, 3] ]
...(14)> odd_list_elements = fn x when is_number(x) -> rem(x,2) == 1
...(14)> x -> is_list(x) end
...(14)> Traverse.filter(ds, odd_list_elements)
[ [3] ]
As with `map` and `walk` there is the bang version, accepting partial filter functions
iex(15)> number_arrays = fn x when is_number(x) -> true
...(15)> l when is_list(l) -> true end
...(15)> Traverse.filter!([:a, {1, 2}, 3, [4, :b]], number_arrays)
[3, [4]]
"""
@spec walk(any, any, t_simple_walker_fn, Keyword.t()) :: any
def walk(ds, initial_acc, walker_fn, options \\ [])
def walk(ds, initial_acc, walker_fn, postwalk: true),
do: Traverse.Walker.postwalk(ds, initial_acc, walker_fn)
def walk(ds, initial_acc, walker_fn, _),
do: Traverse.Walker.walk(ds, initial_acc, walker_fn)
def walk!(ds, initial_acc, walker_fn, options \\ [])
def walk!(ds, initial_acc, walker_fn, postwalk: true),
do: Traverse.Walker.postwalk!(ds, initial_acc, walker_fn)
def walk!(ds, initial_acc, walker_fn, _),
do: Traverse.Walker.walk!(ds, initial_acc, walker_fn)
@spec filter(any, t_simple_filter_fn) :: any
def filter(ds, filter_fn),
do: Traverse.Filter.filter(ds, filter_fn)
@spec filter!(any, t_simple_filter_fn) :: any
def filter!(ds, filter_fn),
do: Traverse.Filter.filter!(ds, filter_fn)
@spec map(any, t_simple_mapper_fn) :: any
def map(ds, mapper_fn),
do: Traverse.Mapper.map(ds, mapper_fn)
@spec map!(any, t_simple_mapper_fn) :: any
def map!(ds, mapper_fn),
do: Traverse.Mapper.map!(ds, mapper_fn)
@spec mapall(any, t_simple_mapper_fn, Keyword.t()) :: any
def mapall(ds, mapper_fn, options \\ []),
do: Traverse.Mapper.mapall(ds, mapper_fn, Keyword.get(options, :post, false))
# @doc """
# `zipfn` augments each node and leaf in the data structure, replacing it with the pair
# containing its original value and the result of the function applied to the node.
# As very often we will be interested in only some specific values we can, as usually,
# define a partial zip function, the `default` value is used to complete the zip
# function with a constant function returning this value, the `default` defaults to nil.
# iex> Tranverse.zip([1, {:a, 2}, %{b: 3, c: "hello"}],
# ...> fn x when is_number(x) -> x + 1 end)
# [{1, 2}, {a: 2]end
# """
# @spec zip( any, t_simple_mapper_fn, any ) :: any
# def zip(ds, zipfn, default \\ nil)
end
# SPDX-License-Identifier: Apache-2.0