Current section

Files

Jump to
yog_ex lib yog traversal implicit.ex
Raw

lib/yog/traversal/implicit.ex

defmodule Yog.Traversal.Implicit do
@moduledoc """
Implicit graph traversal — BFS, DFS, Best-First, and Random on graphs defined by
successor functions rather than materialized data structures.
"""
alias Yog.PairingHeap, as: PQ
@doc """
Traverse implicit graphs using BFS, DFS, Best-First, or Random order
without materializing a `Graph`.
## Options
- `:from`: Starting node.
- `:using`: Traversal order. Options:
- `:breadth_first` (BFS)
- `:depth_first` (DFS)
- `:best_first` - Prioritizes discovery based on a `:priority` function.
- `:random` - Randomizes discovery order.
- `:priority`: Required if `:using` is `:best_first`. A function taking `(node_id, meta)`.
- `:successors_of`: Function returning `[node_id]`.
- `:initial`: Initial accumulator.
- `:with`: Folder function `(acc, node_id, meta)`.
"""
@spec implicit_fold(keyword()) :: any()
def implicit_fold(opts) do
from = Keyword.fetch!(opts, :from)
order = Keyword.fetch!(opts, :using)
initial = Keyword.fetch!(opts, :initial)
successors = Keyword.fetch!(opts, :successors_of)
folder = Keyword.fetch!(opts, :with)
start_meta = %{depth: 0, parent: nil}
case order do
:breadth_first ->
do_implicit_bfs(
:queue.in({from, start_meta}, :queue.new()),
MapSet.new(),
initial,
successors,
folder
)
:depth_first ->
do_implicit_dfs(
[{from, start_meta}],
MapSet.new(),
initial,
successors,
folder
)
:best_first ->
priority_fn = Keyword.fetch!(opts, :priority)
do_implicit_best_first(
PQ.new(fn {p1, _}, {p2, _} -> p1 <= p2 end)
|> PQ.push({0, {from, start_meta}}),
MapSet.new(),
initial,
successors,
folder,
priority_fn
)
:random ->
do_implicit_best_first(
PQ.new(fn {p1, _}, {p2, _} -> p1 <= p2 end)
|> PQ.push({0, {from, start_meta}}),
MapSet.new(),
initial,
successors,
folder,
fn _id, _meta -> :rand.uniform() end
)
end
end
@doc """
Like `implicit_fold/1`, but deduplicates visited nodes by a custom key.
"""
@spec implicit_fold_by(keyword()) :: any()
def implicit_fold_by(opts) do
from = Keyword.fetch!(opts, :from)
order = Keyword.fetch!(opts, :using)
initial = Keyword.fetch!(opts, :initial)
successors = Keyword.fetch!(opts, :successors_of)
key_fn = Keyword.fetch!(opts, :visited_by)
folder = Keyword.fetch!(opts, :with)
start_meta = %{depth: 0, parent: nil}
case order do
:breadth_first ->
do_implicit_bfs_by(
:queue.in({from, start_meta}, :queue.new()),
MapSet.new(),
initial,
successors,
key_fn,
folder
)
:depth_first ->
do_implicit_dfs_by(
[{from, start_meta}],
MapSet.new(),
initial,
successors,
key_fn,
folder
)
:best_first ->
priority_fn = Keyword.fetch!(opts, :priority)
do_implicit_best_first_by(
PQ.new(fn {p1, _}, {p2, _} -> p1 <= p2 end)
|> PQ.push({0, {from, start_meta}}),
MapSet.new(),
initial,
successors,
key_fn,
folder,
priority_fn
)
:random ->
do_implicit_best_first_by(
PQ.new(fn {p1, _}, {p2, _} -> p1 <= p2 end)
|> PQ.push({0, {from, start_meta}}),
MapSet.new(),
initial,
successors,
key_fn,
folder,
fn _id, _meta -> :rand.uniform() end
)
end
end
# Implicit BFS
defp do_implicit_bfs(q, visited, acc, successors, folder) do
case :queue.out(q) do
{:empty, _} ->
acc
{{:value, {node_id, metadata}}, rest} ->
if MapSet.member?(visited, node_id) do
do_implicit_bfs(rest, visited, acc, successors, folder)
else
{control, new_acc} = folder.(acc, node_id, metadata)
new_visited = MapSet.put(visited, node_id)
case control do
:halt ->
new_acc
:stop ->
do_implicit_bfs(rest, new_visited, new_acc, successors, folder)
:continue ->
succ_list = successors.(node_id)
next_queue =
List.foldl(succ_list, rest, fn next_id, q2 ->
:queue.in(
{next_id, %{depth: metadata.depth + 1, parent: node_id}},
q2
)
end)
do_implicit_bfs(next_queue, new_visited, new_acc, successors, folder)
end
end
end
end
# Implicit DFS
defp do_implicit_dfs(stack, visited, acc, successors, folder) do
case stack do
[] ->
acc
[{node_id, metadata} | tail] ->
if MapSet.member?(visited, node_id) do
do_implicit_dfs(tail, visited, acc, successors, folder)
else
{control, new_acc} = folder.(acc, node_id, metadata)
new_visited = MapSet.put(visited, node_id)
case control do
:halt ->
new_acc
:stop ->
do_implicit_dfs(tail, new_visited, new_acc, successors, folder)
:continue ->
succ_list = successors.(node_id)
next_stack =
List.foldl(Enum.reverse(succ_list), tail, fn next_id, stk ->
[{next_id, %{depth: metadata.depth + 1, parent: node_id}} | stk]
end)
do_implicit_dfs(next_stack, new_visited, new_acc, successors, folder)
end
end
end
end
# Implicit BFS with custom key function for deduplication
defp do_implicit_bfs_by(q, visited, acc, successors, key_fn, folder) do
case :queue.out(q) do
{:empty, _} ->
acc
{{:value, {node_id, metadata}}, rest} ->
node_key = key_fn.(node_id)
if MapSet.member?(visited, node_key) do
do_implicit_bfs_by(rest, visited, acc, successors, key_fn, folder)
else
{control, new_acc} = folder.(acc, node_id, metadata)
new_visited = MapSet.put(visited, node_key)
case control do
:halt ->
new_acc
:stop ->
do_implicit_bfs_by(rest, new_visited, new_acc, successors, key_fn, folder)
:continue ->
succ_list = successors.(node_id)
next_queue =
List.foldl(succ_list, rest, fn next_id, q2 ->
:queue.in(
{next_id, %{depth: metadata.depth + 1, parent: node_id}},
q2
)
end)
do_implicit_bfs_by(next_queue, new_visited, new_acc, successors, key_fn, folder)
end
end
end
end
# Implicit DFS with custom key function for deduplication
defp do_implicit_dfs_by(stack, visited, acc, successors, key_fn, folder) do
case stack do
[] ->
acc
[{node_id, metadata} | tail] ->
node_key = key_fn.(node_id)
if MapSet.member?(visited, node_key) do
do_implicit_dfs_by(tail, visited, acc, successors, key_fn, folder)
else
{control, new_acc} = folder.(acc, node_id, metadata)
new_visited = MapSet.put(visited, node_key)
case control do
:halt ->
new_acc
:stop ->
do_implicit_dfs_by(tail, new_visited, new_acc, successors, key_fn, folder)
:continue ->
succ_list = successors.(node_id)
next_stack =
List.foldl(Enum.reverse(succ_list), tail, fn next_id, stk ->
[{next_id, %{depth: metadata.depth + 1, parent: node_id}} | stk]
end)
do_implicit_dfs_by(next_stack, new_visited, new_acc, successors, key_fn, folder)
end
end
end
end
defp do_implicit_best_first(pq, visited, acc, successors, folder, priority_fn) do
if PQ.empty?(pq) do
acc
else
{:ok, {_priority, {node_id, meta}}, rest_pq} = PQ.pop(pq)
if MapSet.member?(visited, node_id) do
do_implicit_best_first(rest_pq, visited, acc, successors, folder, priority_fn)
else
{control, new_acc} = folder.(acc, node_id, meta)
new_visited = MapSet.put(visited, node_id)
case control do
:halt ->
new_acc
:stop ->
do_implicit_best_first(rest_pq, new_visited, new_acc, successors, folder, priority_fn)
:continue ->
succ_list = successors.(node_id)
next_pq =
List.foldl(succ_list, rest_pq, fn next_id, q_acc ->
next_meta = %{depth: meta.depth + 1, parent: node_id}
p = priority_fn.(next_id, next_meta)
PQ.push(q_acc, {p, {next_id, next_meta}})
end)
do_implicit_best_first(next_pq, new_visited, new_acc, successors, folder, priority_fn)
end
end
end
end
defp do_implicit_best_first_by(pq, visited, acc, successors, key_fn, folder, priority_fn) do
if PQ.empty?(pq) do
acc
else
{:ok, {_priority, {node_id, meta}}, rest_pq} = PQ.pop(pq)
node_key = key_fn.(node_id)
if MapSet.member?(visited, node_key) do
do_implicit_best_first_by(rest_pq, visited, acc, successors, key_fn, folder, priority_fn)
else
{control, new_acc} = folder.(acc, node_id, meta)
new_visited = MapSet.put(visited, node_key)
case control do
:halt ->
new_acc
:stop ->
do_implicit_best_first_by(
rest_pq,
new_visited,
new_acc,
successors,
key_fn,
folder,
priority_fn
)
:continue ->
succ_list = successors.(node_id)
next_pq =
List.foldl(succ_list, rest_pq, fn next_id, q_acc ->
next_meta = %{depth: meta.depth + 1, parent: node_id}
p = priority_fn.(next_id, next_meta)
PQ.push(q_acc, {p, {next_id, next_meta}})
end)
do_implicit_best_first_by(
next_pq,
new_visited,
new_acc,
successors,
key_fn,
folder,
priority_fn
)
end
end
end
end
end