Current section
Files
Jump to
Current section
Files
src/yog@builder@labeled.erl
-module(yog@builder@labeled).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/yog/builder/labeled.gleam").
-export([new/1, directed/0, undirected/0, ensure_node/2, add_node/2, add_edge/4, add_unweighted_edge/3, add_simple_edge/3, get_id/2, to_graph/1, from_list/2, from_unweighted_list/2, to_registry/1, next_id/1, all_labels/1, successors/2, predecessors/2]).
-export_type([builder/2]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" A builder for creating graphs using arbitrary labels instead of integer IDs.\n"
"\n"
" This module provides a convenient way to build graphs when your nodes are\n"
" naturally identified by strings or other types, rather than integers. The\n"
" builder maintains a mapping from labels to internal integer IDs and\n"
" converts to a standard `Graph` when needed.\n"
"\n"
" ## Important Usage Notes\n"
"\n"
" ### One-Way Conversion\n"
"\n"
" The `to_graph()` function returns a standard `Graph` that is **detached** from\n"
" the builder. If you modify the graph after conversion (e.g., via `model.add_node`\n"
" or `model.add_edge`), the builder's internal mapping will become out of sync.\n"
" \n"
" **Correct workflow:** Build completely → Convert → Use IDs for algorithms.\n"
" Do NOT modify the resulting graph and expect the builder to track changes.\n"
"\n"
" > **Need incremental updates?** If you want to add/remove nodes and edges\n"
" > incrementally after initial construction, consider using `yog/builder/live`\n"
" > instead. The `LiveBuilder` maintains synchronization between labels and\n"
" > the graph, allowing efficient O(ΔE) updates rather than rebuilding.\n"
"\n"
" ### Stable ID Assignment (Idempotent)\n"
"\n"
" Node IDs are assigned deterministically based on first occurrence. Adding the\n"
" same label multiple times will always return the same ID:\n"
"\n"
" ```gleam\n"
" let builder =\n"
" labeled.directed()\n"
" |> labeled.add_edge(\"A\", \"B\", 10) // A=0, B=1\n"
" |> labeled.add_edge(\"B\", \"C\", 20) // C=2 (B still 1)\n"
" |> labeled.add_edge(\"A\", \"C\", 30) // All IDs stable\n"
" ```\n"
"\n"
" This stability means you can reliably call `get_id()` at any point and get\n"
" consistent results. It also enables incremental graph building from multiple\n"
" sources without worrying about duplicate labels.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import yog/builder/labeled\n"
" import yog/pathfinding/dijkstra as pathfinding\n"
" import gleam/int\n"
"\n"
" pub fn main() {\n"
" // Build a graph using string labels\n"
" let builder =\n"
" labeled.directed()\n"
" |> labeled.add_edge(\"home\", \"work\", 10)\n"
" |> labeled.add_edge(\"work\", \"gym\", 5)\n"
" |> labeled.add_edge(\"home\", \"gym\", 12)\n"
"\n"
" // Convert to a Graph to use with algorithms\n"
" let graph = labeled.to_graph(builder)\n"
"\n"
" // Get the node IDs for the labels we care about\n"
" let assert Ok(home_id) = labeled.get_id(builder, \"home\")\n"
" let assert Ok(gym_id) = labeled.get_id(builder, \"gym\")\n"
"\n"
" // Now use standard graph algorithms\n"
" case pathfinding.shortest_path(\n"
" in: graph,\n"
" from: home_id,\n"
" to: gym_id,\n"
" with_zero: 0,\n"
" with_add: int.add,\n"
" with_compare: int.compare,\n"
" ) {\n"
" Ok(path) -> // Path found!\n"
" Error(_) -> // No path\n"
" }\n"
" }\n"
" ```\n"
).
-type builder(HYI, HYJ) :: {builder,
yog@model:graph(HYI, HYJ),
gleam@dict:dict(HYI, integer()),
integer()}.
-file("src/yog/builder/labeled.gleam", 116).
?DOC(
" Creates a new empty labeled graph builder.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import yog/builder/labeled\n"
" import yog/model.{Directed}\n"
"\n"
" let builder = labeled.new(Directed)\n"
" ```\n"
).
-spec new(yog@model:graph_type()) -> builder(any(), any()).
new(Graph_type) ->
{builder, yog@model:new(Graph_type), maps:new(), 0}.
-file("src/yog/builder/labeled.gleam", 133).
?DOC(
" Creates a new empty labeled directed graph builder.\n"
"\n"
" This is a convenience function equivalent to `labeled.new(Directed)`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import yog/builder/labeled\n"
"\n"
" let builder =\n"
" labeled.directed()\n"
" |> labeled.add_edge(\"home\", \"work\", 10)\n"
" ```\n"
).
-spec directed() -> builder(any(), any()).
directed() ->
{builder, yog@model:new(directed), maps:new(), 0}.
-file("src/yog/builder/labeled.gleam", 150).
?DOC(
" Creates a new empty labeled undirected graph builder.\n"
"\n"
" This is a convenience function equivalent to `labeled.new(Undirected)`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import yog/builder/labeled\n"
"\n"
" let builder =\n"
" labeled.undirected()\n"
" |> labeled.add_edge(\"A\", \"B\", 5)\n"
" ```\n"
).
-spec undirected() -> builder(any(), any()).
undirected() ->
{builder, yog@model:new(undirected), maps:new(), 0}.
-file("src/yog/builder/labeled.gleam", 178).
?DOC(
" Gets or creates a node for the given label, returning the builder and node ID.\n"
"\n"
" If a node with this label already exists, returns its ID without modification.\n"
" If it doesn't exist, creates a new node with the label as its data.\n"
"\n"
" > **Note:** This function is idempotent - calling it multiple times with the\n"
" > same label always returns the same ID. This provides stability when building\n"
" > graphs incrementally from multiple sources.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let #(builder, node_a) = labeled.ensure_node(builder, \"Node A\")\n"
" let #(builder, node_b) = labeled.ensure_node(builder, \"Node B\")\n"
" // Now you have the IDs and can use them with lower-level operations\n"
" ```\n"
).
-spec ensure_node(builder(HYW, HYX), HYW) -> {builder(HYW, HYX), integer()}.
ensure_node(Builder, Label) ->
case gleam_stdlib:map_get(erlang:element(3, Builder), Label) of
{ok, Id} ->
{Builder, Id};
{error, _} ->
Id@1 = erlang:element(4, Builder),
New_graph = yog@model:add_node(
erlang:element(2, Builder),
Id@1,
Label
),
New_mapping = gleam@dict:insert(
erlang:element(3, Builder),
Label,
Id@1
),
{{builder, New_graph, New_mapping, Id@1 + 1}, Id@1}
end.
-file("src/yog/builder/labeled.gleam", 209).
?DOC(
" Adds a node with the given label explicitly.\n"
"\n"
" If a node with this label already exists, its data will be replaced.\n"
" This is useful when you want to add nodes before adding edges.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" builder\n"
" |> labeled.add_node(\"Node A\")\n"
" |> labeled.add_node(\"Node B\")\n"
" |> labeled.add_edge(\"Node A\", \"Node B\", 5)\n"
" ```\n"
).
-spec add_node(builder(HZC, HZD), HZC) -> builder(HZC, HZD).
add_node(Builder, Label) ->
{New_builder, _} = ensure_node(Builder, Label),
New_builder.
-file("src/yog/builder/labeled.gleam", 227).
?DOC(
" Adds an edge between two labeled nodes.\n"
"\n"
" If either node doesn't exist, it will be created automatically.\n"
" For directed graphs, adds a single edge from `from` to `to`.\n"
" For undirected graphs, adds edges in both directions.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" builder\n"
" |> labeled.add_edge(from: \"A\", to: \"B\", with: 10)\n"
" |> labeled.add_edge(from: \"B\", to: \"C\", with: 5)\n"
" ```\n"
).
-spec add_edge(builder(HZI, HZJ), HZI, HZI, HZJ) -> builder(HZI, HZJ).
add_edge(Builder, Src_label, Dst_label, Weight) ->
{Builder@1, Src_id} = ensure_node(Builder, Src_label),
{Builder@2, Dst_id} = ensure_node(Builder@1, Dst_label),
New_graph@1 = case yog@model:add_edge(
erlang:element(2, Builder@2),
Src_id,
Dst_id,
Weight
) of
{ok, New_graph} -> New_graph;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"yog/builder/labeled"/utf8>>,
function => <<"add_edge"/utf8>>,
line => 236,
value => _assert_fail,
start => 7660,
'end' => 7760,
pattern_start => 7671,
pattern_end => 7684})
end,
{builder,
New_graph@1,
erlang:element(3, Builder@2),
erlang:element(4, Builder@2)}.
-file("src/yog/builder/labeled.gleam", 256).
?DOC(
" Adds an unweighted edge between two labeled nodes.\n"
"\n"
" This is a convenience function for graphs where edges have no meaningful weight.\n"
" Uses `Nil` as the edge data type. Nodes are created automatically if they don't exist.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import yog/builder/labeled\n"
"\n"
" let builder: labeled.Builder(String, Nil) = labeled.directed()\n"
" |> labeled.add_unweighted_edge(\"A\", \"B\")\n"
" |> labeled.add_unweighted_edge(\"B\", \"C\")\n"
" ```\n"
).
-spec add_unweighted_edge(builder(HZO, nil), HZO, HZO) -> builder(HZO, nil).
add_unweighted_edge(Builder, Src_label, Dst_label) ->
{Builder@1, Src_id} = ensure_node(Builder, Src_label),
{Builder@2, Dst_id} = ensure_node(Builder@1, Dst_label),
New_graph@1 = case yog@model:add_edge(
erlang:element(2, Builder@2),
Src_id,
Dst_id,
nil
) of
{ok, New_graph} -> New_graph;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"yog/builder/labeled"/utf8>>,
function => <<"add_unweighted_edge"/utf8>>,
line => 264,
value => _assert_fail,
start => 8535,
'end' => 8632,
pattern_start => 8546,
pattern_end => 8559})
end,
{builder,
New_graph@1,
erlang:element(3, Builder@2),
erlang:element(4, Builder@2)}.
-file("src/yog/builder/labeled.gleam", 286).
?DOC(
" Adds a simple edge with weight 1 between two labeled nodes.\n"
"\n"
" This is a convenience function for graphs with integer weights where\n"
" a default weight of 1 is appropriate (e.g., unweighted graphs, hop counts).\n"
" Nodes are created automatically if they don't exist.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import yog/builder/labeled\n"
"\n"
" let builder = labeled.directed()\n"
" |> labeled.add_simple_edge(\"home\", \"work\")\n"
" |> labeled.add_simple_edge(\"work\", \"gym\")\n"
" // Both edges have weight 1\n"
" ```\n"
).
-spec add_simple_edge(builder(HZT, integer()), HZT, HZT) -> builder(HZT, integer()).
add_simple_edge(Builder, Src_label, Dst_label) ->
{Builder@1, Src_id} = ensure_node(Builder, Src_label),
{Builder@2, Dst_id} = ensure_node(Builder@1, Dst_label),
New_graph@1 = case yog@model:add_edge(
erlang:element(2, Builder@2),
Src_id,
Dst_id,
1
) of
{ok, New_graph} -> New_graph;
_assert_fail ->
erlang:error(#{gleam_error => let_assert,
message => <<"Pattern match failed, no pattern matched the value."/utf8>>,
file => <<?FILEPATH/utf8>>,
module => <<"yog/builder/labeled"/utf8>>,
function => <<"add_simple_edge"/utf8>>,
line => 294,
value => _assert_fail,
start => 9451,
'end' => 9546,
pattern_start => 9462,
pattern_end => 9475})
end,
{builder,
New_graph@1,
erlang:element(3, Builder@2),
erlang:element(4, Builder@2)}.
-file("src/yog/builder/labeled.gleam", 316).
?DOC(
" Looks up the internal node ID for a given label.\n"
"\n"
" Returns `Ok(id)` if the label exists, `Error(Nil)` if it doesn't.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" case labeled.get_id(builder, \"Node A\") {\n"
" Ok(id) -> // Use the ID\n"
" Error(_) -> // Label doesn't exist\n"
" }\n"
" ```\n"
).
-spec get_id(builder(HZY, any()), HZY) -> {ok, integer()} | {error, nil}.
get_id(Builder, Label) ->
gleam_stdlib:map_get(erlang:element(3, Builder), Label).
-file("src/yog/builder/labeled.gleam", 340).
?DOC(
" Converts the builder to a standard `Graph`.\n"
"\n"
" The resulting graph uses integer IDs internally and stores the labels\n"
" as node data. This graph can be used with all yog algorithms.\n"
"\n"
" > **Warning:** The returned graph is a snapshot. Modifying it directly\n"
" > (e.g., via `model.add_node` or `model.add_edge`) will NOT update the\n"
" > builder, and `get_id` will return stale information. Complete all\n"
" > building operations before converting.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let graph = labeled.to_graph(builder)\n"
" // Now use with pathfinding, traversal, etc.\n"
" ```\n"
).
-spec to_graph(builder(IAE, IAF)) -> yog@model:graph(IAE, IAF).
to_graph(Builder) ->
erlang:element(2, Builder).
-file("src/yog/builder/labeled.gleam", 355).
?DOC(
" Creates a labeled graph builder from a list of edges #(src_label, dst_label, weight).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let builder = labeled.from_list(model.Directed, [#(\"A\", \"B\", 10), #(\"B\", \"C\", 5)])\n"
" ```\n"
).
-spec from_list(yog@model:graph_type(), list({IAK, IAK, IAL})) -> builder(IAK, IAL).
from_list(Graph_type, Edges) ->
gleam@list:fold(
Edges,
new(Graph_type),
fun(B, _use1) ->
{Src, Dst, Weight} = _use1,
add_edge(B, Src, Dst, Weight)
end
).
-file("src/yog/builder/labeled.gleam", 370).
?DOC(
" Creates a labeled graph builder from a list of unweighted edges #(src_label, dst_label).\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let builder = labeled.from_unweighted_list(model.Directed, [#(\"A\", \"B\"), #(\"B\", \"C\")])\n"
" ```\n"
).
-spec from_unweighted_list(yog@model:graph_type(), list({IAP, IAP})) -> builder(IAP, nil).
from_unweighted_list(Graph_type, Edges) ->
gleam@list:fold(
Edges,
new(Graph_type),
fun(B, _use1) ->
{Src, Dst} = _use1,
add_unweighted_edge(B, Src, Dst)
end
).
-file("src/yog/builder/labeled.gleam", 391).
?DOC(
" Extracts the label-to-ID registry from the builder.\n"
"\n"
" This is primarily used for migrating to a `LiveBuilder`. The registry\n"
" preserves all ID mappings, allowing seamless transition from static to\n"
" incremental building.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let static = labeled.directed() |> labeled.add_edge(\"A\", \"B\", 10)\n"
" let registry = labeled.to_registry(static)\n"
" // Use with live.from_registry(registry)\n"
" ```\n"
).
-spec to_registry(builder(IAT, any())) -> gleam@dict:dict(IAT, integer()).
to_registry(Builder) ->
erlang:element(3, Builder).
-file("src/yog/builder/labeled.gleam", 402).
?DOC(
" Returns the next available ID from the builder.\n"
"\n"
" Used in conjunction with `to_registry` for migrating to `LiveBuilder`.\n"
).
-spec next_id(builder(any(), any())) -> integer().
next_id(Builder) ->
erlang:element(4, Builder).
-file("src/yog/builder/labeled.gleam", 414).
?DOC(
" Returns all labels that have been added to the builder.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" let labels = labeled.all_labels(builder)\n"
" // [\"Node A\", \"Node B\", \"Node C\"]\n"
" ```\n"
).
-spec all_labels(builder(IBD, any())) -> list(IBD).
all_labels(Builder) ->
maps:keys(erlang:element(3, Builder)).
-file("src/yog/builder/labeled.gleam", 469).
-spec list_map_ids_to_labels(list({integer(), IBW}), yog@model:graph(IBY, IBW)) -> list({IBY,
IBW}).
list_map_ids_to_labels(Edges, Graph) ->
_pipe = Edges,
gleam@list:filter_map(
_pipe,
fun(Edge) ->
{Node_id, Edge_data} = Edge,
case gleam_stdlib:map_get(erlang:element(3, Graph), Node_id) of
{ok, Label} ->
{ok, {Label, Edge_data}};
{error, _} ->
{error, nil}
end
end
).
-file("src/yog/builder/labeled.gleam", 430).
?DOC(
" Gets the successors of a node by its label.\n"
"\n"
" Returns a list of tuples containing the successor's label and edge data.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" case labeled.successors(builder, \"Node A\") {\n"
" Ok(successors) -> // List of #(label, edge_data)\n"
" Error(_) -> // Node doesn't exist\n"
" }\n"
" ```\n"
).
-spec successors(builder(IBI, IBJ), IBI) -> {ok, list({IBI, IBJ})} |
{error, nil}.
successors(Builder, Label) ->
gleam@result:'try'(
get_id(Builder, Label),
fun(Id) ->
Successor_edges = yog@model:successors(
erlang:element(2, Builder),
Id
),
_pipe = Successor_edges,
_pipe@1 = list_map_ids_to_labels(_pipe, erlang:element(2, Builder)),
{ok, _pipe@1}
end
).
-file("src/yog/builder/labeled.gleam", 455).
?DOC(
" Gets the predecessors of a node by its label.\n"
"\n"
" Returns a list of tuples containing the predecessor's label and edge data.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" case labeled.predecessors(builder, \"Node A\") {\n"
" Ok(predecessors) -> // List of #(label, edge_data)\n"
" Error(_) -> // Node doesn't exist\n"
" }\n"
" ```\n"
).
-spec predecessors(builder(IBP, IBQ), IBP) -> {ok, list({IBP, IBQ})} |
{error, nil}.
predecessors(Builder, Label) ->
gleam@result:'try'(
get_id(Builder, Label),
fun(Id) ->
Predecessor_edges = yog@model:predecessors(
erlang:element(2, Builder),
Id
),
_pipe = Predecessor_edges,
_pipe@1 = list_map_ids_to_labels(_pipe, erlang:element(2, Builder)),
{ok, _pipe@1}
end
).