yog
Yog - A comprehensive graph algorithm library for Gleam.
Provides efficient implementations of classic graph algorithms with a clean, functional API.
Quick Start
import yog
import yog/pathfinding/dijkstra as pathfinding
import gleam/int
pub fn main() {
let graph =
yog.directed()
|> yog.add_node(1, "Start")
|> yog.add_node(2, "Middle")
|> yog.add_node(3, "End")
|> yog.add_edge(from: 1, to: 2, with: 5)
|> yog.add_edge(from: 2, to: 3, with: 3)
|> yog.add_edge(from: 1, to: 3, with: 10)
case pathfinding.shortest_path(
in: graph,
from: 1,
to: 3,
with_zero: 0,
with_add: int.add,
with_compare: int.compare
) {
Some(path) -> {
// Path(nodes: [1, 2, 3], total_weight: 8)
io.println("Shortest path found!")
}
None -> io.println("No path exists")
}
}
Modules
Core
-
yog/model- Graph data structures and basic operations- Create directed/undirected graphs
- Add nodes and edges
- Query successors, predecessors, neighbors
-
yog/builder/labeled- Build graphs with arbitrary labels- Use strings or any type as node identifiers
- Automatically maps labels to internal integer IDs
- Convert to standard Graph for use with all algorithms
Algorithms
-
yog/pathfinding- Shortest path algorithms- Dijkstra’s algorithm (non-negative weights)
- A* search (with heuristics)
- Bellman-Ford (negative weights, cycle detection)
-
yog/traversal- Graph traversal- Breadth-First Search (BFS)
- Depth-First Search (DFS)
- Early termination support
-
yog/mst- Minimum Spanning Tree- Kruskal’s algorithm with Union-Find
- Prim’s algorithm with priority queue
-
yog/traversal- Topological ordering- Kahn’s algorithm
- Lexicographical variant (heap-based)
-
yog/connectivity- Connected components- Tarjan’s algorithm for Strongly Connected Components (SCC)
- Kosaraju’s algorithm for SCC (two-pass with transpose)
-
yog/connectivity- Graph connectivity analysis- Tarjan’s algorithm for bridges and articulation points
-
yog/flow- Minimum cut algorithms- Stoer-Wagner algorithm for global minimum cut
-
yog/properties- Eulerian paths and circuits- Detection of Eulerian paths and circuits
- Hierholzer’s algorithm for finding paths
- Works on both directed and undirected graphs
-
yog/properties- Bipartite graph detection and matching- Bipartite detection (2-coloring)
- Partition extraction (independent sets)
- Maximum matching (augmenting path algorithm)
Data Structures
yog/disjoint_set- Union-Find / Disjoint Set- Path compression and union by rank
- O(α(n)) amortized operations (practically constant)
- Dynamic connectivity queries
- Generic over any type
Transformations
yog/transform- Graph transformations- Transpose (O(1) edge reversal!)
- Map nodes and edges (functor operations)
- Filter nodes with auto-pruning
- Merge graphs
Visualization
yog/render- Graph visualization- Mermaid diagram generation (GitHub/GitLab compatible)
- Path highlighting for algorithm results
- Customizable node and edge labels
Features
- Functional and Immutable: All operations return new graphs
- Generic: Works with any node/edge data types
- Type-Safe: Leverages Gleam’s type system
- Well-Tested: 494+ tests covering all algorithms and data structures
- Efficient: Optimal data structures (pairing heaps, union-find)
- Documented: Every function has examples
Types
pub type Graph(node_data, edge_data) =
model.Graph(node_data, edge_data)
pub type GraphType =
model.GraphType
pub type Order =
traversal.Order
pub type WalkControl =
traversal.WalkControl
pub type WalkMetadata(nid) =
traversal.WalkMetadata(nid)
Values
pub fn add_edge(
graph: model.Graph(n, e),
from src: Int,
to dst: Int,
with weight: e,
) -> model.Graph(n, e)
Adds an edge to the graph with the given weight.
For directed graphs, adds a single edge from src to dst.
For undirected graphs, adds edges in both directions.
Example
graph
|> yog.add_edge(from: 1, to: 2, with: 10)
pub fn add_edge_ensured(
graph: model.Graph(n, e),
from src: Int,
to dst: Int,
with weight: e,
default default: n,
) -> model.Graph(n, e)
Like add_edge, but ensures both endpoint nodes exist first.
If src or dst is not already in the graph, it is created with
the supplied default node data. Existing nodes are left unchanged.
Example
yog.directed()
|> yog.add_edge_ensured(from: 1, to: 2, with: 10, default: "anon")
// Nodes 1 and 2 are auto-created with data "anon"
pub fn add_node(
graph: model.Graph(n, e),
id: Int,
data: n,
) -> model.Graph(n, e)
Adds a node to the graph with the given ID and data. If a node with this ID already exists, its data will be replaced.
Example
graph
|> yog.add_node(1, "Node A")
|> yog.add_node(2, "Node B")
pub fn add_simple_edge(
graph: model.Graph(n, Int),
from src: Int,
to dst: Int,
) -> model.Graph(n, Int)
Adds a simple edge with weight 1.
This is a convenience function for graphs with integer weights where a default weight of 1 is appropriate (e.g., unweighted graphs, hop counts).
Example
graph
|> yog.add_simple_edge(from: 1, to: 2)
|> yog.add_simple_edge(from: 2, to: 3)
// Both edges have weight 1
pub fn add_unweighted_edge(
graph: model.Graph(n, Nil),
from src: Int,
to dst: Int,
) -> model.Graph(n, Nil)
Adds an unweighted edge to the graph.
This is a convenience function for graphs where edges have no meaningful weight.
Uses Nil as the edge data type.
Example
let graph: Graph(String, Nil) = yog.directed()
|> yog.add_node(1, "A")
|> yog.add_node(2, "B")
|> yog.add_unweighted_edge(from: 1, to: 2)
pub fn all_nodes(graph: model.Graph(n, e)) -> List(Int)
Returns all unique node IDs that have edges in the graph.
pub const breadth_first: traversal.Order
pub fn complement(
graph: model.Graph(n, e),
default_weight default_weight: e,
) -> model.Graph(n, e)
pub const continue: traversal.WalkControl
pub fn contract(
in graph: model.Graph(n, e),
merge a: Int,
with b: Int,
combine_weights with_combine: fn(e, e) -> e,
) -> model.Graph(n, e)
pub const depth_first: traversal.Order
pub fn directed() -> model.Graph(n, e)
Creates a new empty directed graph.
This is a convenience function that’s equivalent to yog.new(Directed),
but requires only a single import.
Example
import yog
let graph =
yog.directed()
|> yog.add_node(1, "Start")
|> yog.add_node(2, "End")
|> yog.add_edge(from: 1, to: 2, with: 10)
pub fn filter_edges(
graph: model.Graph(n, e),
keeping predicate: fn(Int, Int, e) -> Bool,
) -> model.Graph(n, e)
pub fn filter_nodes(
graph: model.Graph(n, e),
keeping predicate: fn(n) -> Bool,
) -> model.Graph(n, e)
pub fn fold_walk(
over graph: model.Graph(n, e),
from start: Int,
using order: traversal.Order,
initial acc: a,
with folder: fn(a, Int, traversal.WalkMetadata(Int)) -> #(
traversal.WalkControl,
a,
),
) -> a
pub fn from_adjacency_list(
graph_type: model.GraphType,
adj_list: List(#(Int, List(#(Int, e)))),
) -> model.Graph(Nil, e)
Creates a graph from an adjacency list #(src, List(#(dst, weight))).
Example
let graph = yog.from_adjacency_list(model.Directed, [#(1, [#(2, 10), #(3, 5)])])
pub fn from_edges(
graph_type: model.GraphType,
edges: List(#(Int, Int, e)),
) -> model.Graph(Nil, e)
Creates a graph from a list of edges #(src, dst, weight).
Example
let graph = yog.from_edges(model.Directed, [#(1, 2, 10), #(2, 3, 5)])
pub fn from_unweighted_edges(
graph_type: model.GraphType,
edges: List(#(Int, Int)),
) -> model.Graph(Nil, Nil)
Creates a graph from a list of unweighted edges #(src, dst).
Example
let graph = yog.from_unweighted_edges(model.Directed, [#(1, 2), #(2, 3)])
pub const halt: traversal.WalkControl
pub fn is_acyclic(graph: model.Graph(n, e)) -> Bool
Determines if a graph is acyclic (contains no cycles).
This is the logical opposite of is_cyclic. For directed graphs, returning
True means the graph is a Directed Acyclic Graph (DAG).
Time Complexity: O(V + E)
Example
yog.is_acyclic(graph)
// => True // Valid DAG or undirected forest
pub fn is_cyclic(graph: model.Graph(n, e)) -> Bool
Determines if a graph contains any cycles.
For directed graphs, a cycle exists if there is a path from a node back to itself. For undirected graphs, a cycle exists if there is a path of length >= 3 from a node back to itself, or a self-loop.
Time Complexity: O(V + E)
Example
yog.is_cyclic(graph)
// => True // Cycle detected
pub fn map_edges(
graph: model.Graph(n, e),
with fun: fn(e) -> f,
) -> model.Graph(n, f)
pub fn map_nodes(
graph: model.Graph(n, e),
with fun: fn(n) -> m,
) -> model.Graph(m, e)
pub fn merge(
base: model.Graph(n, e),
other: model.Graph(n, e),
) -> model.Graph(n, e)
pub fn neighbors(
graph: model.Graph(n, e),
id: Int,
) -> List(#(Int, e))
Gets all nodes connected to the given node, regardless of direction. For undirected graphs, this is equivalent to successors. For directed graphs, this combines successors and predecessors.
pub fn new(graph_type: model.GraphType) -> model.Graph(n, e)
Creates a new empty graph of the specified type.
Example
import yog
import yog/model.{Directed}
let graph = yog.new(Directed)
pub fn predecessors(
graph: model.Graph(n, e),
id: Int,
) -> List(#(Int, e))
Gets nodes you came FROM to reach the given node (predecessors). Returns a list of tuples containing the source node ID and edge data.
pub const stop: traversal.WalkControl
pub fn subgraph(
graph: model.Graph(n, e),
keeping ids: List(Int),
) -> model.Graph(n, e)
pub fn successor_ids(
graph: model.Graph(n, e),
id: Int,
) -> List(Int)
Returns just the NodeIds of successors (without edge data). Convenient for traversal algorithms that only need the IDs.
pub fn successors(
graph: model.Graph(n, e),
id: Int,
) -> List(#(Int, e))
Gets nodes you can travel TO from the given node (successors). Returns a list of tuples containing the destination node ID and edge data.
pub fn to_directed(graph: model.Graph(n, e)) -> model.Graph(n, e)
pub fn to_undirected(
graph: model.Graph(n, e),
resolve resolve: fn(e, e) -> e,
) -> model.Graph(n, e)
pub fn transpose(graph: model.Graph(n, e)) -> model.Graph(n, e)
pub fn undirected() -> model.Graph(n, e)
Creates a new empty undirected graph.
This is a convenience function that’s equivalent to yog.new(Undirected),
but requires only a single import.
Example
import yog
let graph =
yog.undirected()
|> yog.add_node(1, "A")
|> yog.add_node(2, "B")
|> yog.add_edge(from: 1, to: 2, with: 5)
pub fn walk(
in graph: model.Graph(n, e),
from start_id: Int,
using order: traversal.Order,
) -> List(Int)
pub fn walk_until(
in graph: model.Graph(n, e),
from start_id: Int,
using order: traversal.Order,
until should_stop: fn(Int) -> Bool,
) -> List(Int)