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/live instead. The LiveBuilder maintains 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_node or model.add_edge) will NOT update the builder, and get_id will 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)
Search Document