yog/builder/labeled
A builder for creating graphs using arbitrary labels instead of integer IDs.
This module provides a convenient way to build graphs when your nodes are
naturally identified by strings or other types, rather than integers. The
builder maintains a mapping from labels to internal integer IDs and
converts to a standard Graph when needed.
Important Usage Notes
One-Way Conversion
The to_graph() function returns a standard Graph that is detached from
the builder. If you modify the graph after conversion (e.g., via model.add_node
or model.add_edge), the builder’s internal mapping will become out of sync.
Correct workflow: Build completely → Convert → Use IDs for algorithms. Do NOT modify the resulting graph and expect the builder to track changes.
Need incremental updates? If you want to add/remove nodes and edges incrementally after initial construction, consider using
yog/builder/liveinstead. TheLiveBuildermaintains synchronization between labels and the graph, allowing efficient O(ΔE) updates rather than rebuilding.
Stable ID Assignment (Idempotent)
Node IDs are assigned deterministically based on first occurrence. Adding the same label multiple times will always return the same ID:
let builder =
labeled.directed()
|> labeled.add_edge("A", "B", 10) // A=0, B=1
|> labeled.add_edge("B", "C", 20) // C=2 (B still 1)
|> labeled.add_edge("A", "C", 30) // All IDs stable
This stability means you can reliably call get_id() at any point and get
consistent results. It also enables incremental graph building from multiple
sources without worrying about duplicate labels.
Example
import yog/builder/labeled
import yog/pathfinding/dijkstra as pathfinding
import gleam/int
pub fn main() {
// Build a graph using string labels
let builder =
labeled.directed()
|> labeled.add_edge("home", "work", 10)
|> labeled.add_edge("work", "gym", 5)
|> labeled.add_edge("home", "gym", 12)
// Convert to a Graph to use with algorithms
let graph = labeled.to_graph(builder)
// Get the node IDs for the labels we care about
let assert Ok(home_id) = labeled.get_id(builder, "home")
let assert Ok(gym_id) = labeled.get_id(builder, "gym")
// Now use standard graph algorithms
case pathfinding.shortest_path(
in: graph,
from: home_id,
to: gym_id,
with_zero: 0,
with_add: int.add,
with_compare: int.compare,
) {
Ok(path) -> // Path found!
Error(_) -> // No path
}
}
Types
A builder for graphs that use arbitrary labels instead of integer node IDs.
The builder maintains an internal mapping from labels to integer IDs and stores the label as the node’s data in the underlying graph.
pub type Builder(label, edge_data) {
Builder(
graph: model.Graph(label, edge_data),
label_to_id: dict.Dict(label, Int),
next_id: Int,
)
}
Constructors
-
Builder( graph: model.Graph(label, edge_data), label_to_id: dict.Dict(label, Int), next_id: Int, )Arguments
- graph
-
The underlying graph structure
- label_to_id
-
Mapping from label to internal node ID
- next_id
-
Next available node ID
Values
pub fn add_edge(
builder: Builder(label, e),
from src_label: label,
to dst_label: label,
with weight: e,
) -> Builder(label, e)
Adds an edge between two labeled nodes.
If either node doesn’t exist, it will be created automatically.
For directed graphs, adds a single edge from from to to.
For undirected graphs, adds edges in both directions.
Example
builder
|> labeled.add_edge(from: "A", to: "B", with: 10)
|> labeled.add_edge(from: "B", to: "C", with: 5)
pub fn add_node(
builder: Builder(label, e),
label: label,
) -> Builder(label, e)
Adds a node with the given label explicitly.
If a node with this label already exists, its data will be replaced. This is useful when you want to add nodes before adding edges.
Example
builder
|> labeled.add_node("Node A")
|> labeled.add_node("Node B")
|> labeled.add_edge("Node A", "Node B", 5)
pub fn add_simple_edge(
builder: Builder(label, Int),
from src_label: label,
to dst_label: label,
) -> Builder(label, Int)
Adds a simple edge with weight 1 between two labeled nodes.
This is a convenience function for graphs with integer weights where a default weight of 1 is appropriate (e.g., unweighted graphs, hop counts). Nodes are created automatically if they don’t exist.
Example
import yog/builder/labeled
let builder = labeled.directed()
|> labeled.add_simple_edge("home", "work")
|> labeled.add_simple_edge("work", "gym")
// Both edges have weight 1
pub fn add_unweighted_edge(
builder: Builder(label, Nil),
from src_label: label,
to dst_label: label,
) -> Builder(label, Nil)
Adds an unweighted edge between two labeled nodes.
This is a convenience function for graphs where edges have no meaningful weight.
Uses Nil as the edge data type. Nodes are created automatically if they don’t exist.
Example
import yog/builder/labeled
let builder: labeled.Builder(String, Nil) = labeled.directed()
|> labeled.add_unweighted_edge("A", "B")
|> labeled.add_unweighted_edge("B", "C")
pub fn all_labels(builder: Builder(label, e)) -> List(label)
Returns all labels that have been added to the builder.
Example
let labels = labeled.all_labels(builder)
// ["Node A", "Node B", "Node C"]
pub fn directed() -> Builder(label, edge_data)
Creates a new empty labeled directed graph builder.
This is a convenience function equivalent to labeled.new(Directed).
Example
import yog/builder/labeled
let builder =
labeled.directed()
|> labeled.add_edge("home", "work", 10)
pub fn ensure_node(
builder: Builder(label, e),
label: label,
) -> #(Builder(label, e), Int)
Gets or creates a node for the given label, returning the builder and node ID.
If a node with this label already exists, returns its ID without modification. If it doesn’t exist, creates a new node with the label as its data.
Note: This function is idempotent - calling it multiple times with the same label always returns the same ID. This provides stability when building graphs incrementally from multiple sources.
Example
let #(builder, node_a) = labeled.ensure_node(builder, "Node A")
let #(builder, node_b) = labeled.ensure_node(builder, "Node B")
// Now you have the IDs and can use them with lower-level operations
pub fn from_list(
graph_type: model.GraphType,
edges: List(#(label, label, e)),
) -> Builder(label, e)
Creates a labeled graph builder from a list of edges #(src_label, dst_label, weight).
Example
let builder = labeled.from_list(model.Directed, [#("A", "B", 10), #("B", "C", 5)])
pub fn from_unweighted_list(
graph_type: model.GraphType,
edges: List(#(label, label)),
) -> Builder(label, Nil)
Creates a labeled graph builder from a list of unweighted edges #(src_label, dst_label).
Example
let builder = labeled.from_unweighted_list(model.Directed, [#("A", "B"), #("B", "C")])
pub fn get_id(
builder: Builder(label, e),
label: label,
) -> Result(Int, Nil)
Looks up the internal node ID for a given label.
Returns Ok(id) if the label exists, Error(Nil) if it doesn’t.
Example
case labeled.get_id(builder, "Node A") {
Ok(id) -> // Use the ID
Error(_) -> // Label doesn't exist
}
pub fn new(
graph_type: model.GraphType,
) -> Builder(label, edge_data)
Creates a new empty labeled graph builder.
Example
import yog/builder/labeled
import yog/model.{Directed}
let builder = labeled.new(Directed)
pub fn next_id(builder: Builder(label, e)) -> Int
Returns the next available ID from the builder.
Used in conjunction with to_registry for migrating to LiveBuilder.
pub fn predecessors(
builder: Builder(label, e),
label: label,
) -> Result(List(#(label, e)), Nil)
Gets the predecessors of a node by its label.
Returns a list of tuples containing the predecessor’s label and edge data.
Example
case labeled.predecessors(builder, "Node A") {
Ok(predecessors) -> // List of #(label, edge_data)
Error(_) -> // Node doesn't exist
}
pub fn successors(
builder: Builder(label, e),
label: label,
) -> Result(List(#(label, e)), Nil)
Gets the successors of a node by its label.
Returns a list of tuples containing the successor’s label and edge data.
Example
case labeled.successors(builder, "Node A") {
Ok(successors) -> // List of #(label, edge_data)
Error(_) -> // Node doesn't exist
}
pub fn to_graph(
builder: Builder(label, e),
) -> model.Graph(label, e)
Converts the builder to a standard Graph.
The resulting graph uses integer IDs internally and stores the labels as node data. This graph can be used with all yog algorithms.
Warning: The returned graph is a snapshot. Modifying it directly (e.g., via
model.add_nodeormodel.add_edge) will NOT update the builder, andget_idwill return stale information. Complete all building operations before converting.
Example
let graph = labeled.to_graph(builder)
// Now use with pathfinding, traversal, etc.
pub fn to_registry(
builder: Builder(label, e),
) -> dict.Dict(label, Int)
Extracts the label-to-ID registry from the builder.
This is primarily used for migrating to a LiveBuilder. The registry
preserves all ID mappings, allowing seamless transition from static to
incremental building.
Example
let static = labeled.directed() |> labeled.add_edge("A", "B", 10)
let registry = labeled.to_registry(static)
// Use with live.from_registry(registry)
pub fn undirected() -> Builder(label, edge_data)
Creates a new empty labeled undirected graph builder.
This is a convenience function equivalent to labeled.new(Undirected).
Example
import yog/builder/labeled
let builder =
labeled.undirected()
|> labeled.add_edge("A", "B", 5)