Clausal Prolog — Graphs (graphs module)
Overview
The graphs module provides predicates for graph creation, traversal, pathfinding, cycle detection, connectivity, and minimum spanning trees. Graphs are represented as edge lists — plain lists matching the pairs convention.
-import_from(graphs, [vertices, shortest_path])
route(V, P) <- (
G is [['a', 'b'], ['b', 'c'], ['a', 'c']],
vertices(G, V),
shortest_path(G, 'a', 'c', P)
)
def main():
for V, P in --route(V, P):
print(f"vertices: {V}") # vertices: ['a', 'b', 'c']
print(f"shortest path: {P}") # shortest path: ['a', 'c']
Or via module import:
-import_module(graphs)
connected_vertices(V) <- (
G is [['a', 'b'], ['b', 'c']],
graphs.vertices(G, V),
graphs.is_connected(G)
)
Import
-import_from(graphs, [
vertices, neighbors, has_edge, degree,
is_connected, is_isolated,
breadth_first_nodes, depth_first_nodes,
find_path, shortest_path, path_cost,
connected_components, topological_sort, has_cycle,
spanning_tree, min_spanning_tree,
reverse_edges, merge_graphs
])
Edge representation
- Unweighted:
[['a', 'b'], ['b', 'c'], ...] — list of 2-element lists
- Weighted:
[['a', 'b', 3], ['b', 'c', 5], ...] — list of 3-element lists
- Lone vertex: a 1-element
[['v']] entry names a vertex with no incident
edge (makes single-vertex graphs representable and is_isolated enumeration
reachable)
- vertices are extracted automatically from edges. A vertex is any term;
the examples use quoted atoms (
'a', an atom in every -double_quotes
mode). A "a" vertex is a string and is kept as one: vertices([["a", "b"]], V)
gives two string vertices, which are not the atoms 'a' and 'b'
- Malformed entries (not a 1-, 2-, or 3-element list) are silently skipped
Directionality and conventions
Most predicates treat the edge list as undirected — neighbors, degree,
find_path, shortest_path, path_cost, is_connected,
connected_components, spanning_tree, and min_spanning_tree all follow an
edge in both directions. The exceptions are directed: has_cycle,
topological_sort, and reverse_edges respect edge direction, and has_edge
matches an edge only in the stored [U, V] order (so has_edge([['a', 'b']],
'b', 'a') fails even though 'a' and 'b' are neighbors).
Other conventions worth noting:
degree counts a self-loop once ([['a', 'a']] → degree 1), not twice.
path_cost requires at least two vertices; a single-vertex path fails
rather than reporting cost 0.
breadth_first_nodes from an absent source yields just [Source] — the
source is treated as a phantom isolated vertex. find_path from an absent
source fails unless Start == End (in which case it yields the
single-vertex path [Start]).
is_isolated in check mode treats any term that is not a vertex as
isolated — is_isolated([['a', 'b']], 'zzz') succeeds, since a non-vertex
trivially has degree 0 (accepted behavior). Enumerate mode only
yields actual vertices.
shortest_path requires non-negative weights (Dijkstra); a graph with a
negative weight fails.
min_spanning_tree/spanning_tree require a connected graph; a
disconnected graph has no spanning tree and fails.
merge_graphs concatenates the two edge lists (duplicates are kept); it
is not a set union.
is_connected([]) is vacuously true (no vertices to disconnect).
- Parallel edges are de-duplicated in adjacency, so
neighbors and find_path
do not report a neighbor or path more than once per distinct edge.
Graph query predicates
| Predicate |
Mode |
Description |
vertices(Edges, Verts) |
+Edges, -Verts |
Extract unique vertex list from edges |
neighbors(Edges, Node, Nbrs) |
+Edges, +Node, -Nbrs |
List of adjacent nodes |
has_edge(Edges, U, V) |
+Edges, ?U, ?V |
Succeeds if a directed edge [U, V] exists (order matters); enumerates on backtrack |
degree(Edges, Node, Deg) |
+Edges, +Node, -Deg |
Count of incident edges |
vertices([['a', 'b'], ['b', 'c']], V) % V = ['a', 'b', 'c']
neighbors([['a', 'b'], ['b', 'c']], 'b', N) % N = ['a', 'c']
degree([['a', 'b'], ['b', 'c']], 'b', D) % D = 2
Graph property predicates
| Predicate |
Mode |
Description |
is_connected(Edges) |
+Edges |
Succeeds if graph is connected |
is_isolated(Edges, Node) |
+Edges, ?Node |
Check or enumerate isolated nodes (degree 0) |
Traversal predicates
| Predicate |
Mode |
Description |
breadth_first_nodes(Edges, Source, Nodes) |
+Edges, +Source, -Nodes |
BFS node ordering from source |
depth_first_nodes(Edges, Source, Nodes) |
+Edges, +Source, -Nodes |
DFS preorder node ordering from source |
breadth_first_nodes([['a', 'b'], ['b', 'c'], ['c', 'd']], 'a', N)
# N = ['a', 'b', 'c', 'd']
Pathfinding predicates
| Predicate |
Mode |
Description |
find_path(Edges, Start, End, Path) |
+Edges, +Start, +End, -Path |
Enumerate all simple paths via backtracking |
shortest_path(Edges, Start, End, Path) |
+Edges, +Start, +End, -Path |
Shortest path (BFS for unweighted, Dijkstra for weighted). Precondition: weighted edges must be non-negative; a graph with any negative weight fails (Dijkstra is unsound with negative weights). |
path_cost(Edges, Path, Cost) |
+Edges, +Path, -Cost |
Sum of edge weights along a path |
# Enumerate all paths
find_path([['a', 'b'], ['b', 'c'], ['a', 'c']], 'a', 'c', P)
# P = ['a', 'b', 'c'] then P = ['a', 'c']
# Shortest path in a weighted graph
shortest_path([['a', 'b', 1], ['b', 'c', 2], ['a', 'c', 10]], 'a', 'c', P)
# P = ['a', 'b', 'c']
# Cost of a path
path_cost([['a', 'b', 3], ['b', 'c', 5]], ['a', 'b', 'c'], C)
# C = 8
Components and ordering predicates
| Predicate |
Mode |
Description |
connected_components(Edges, Components) |
+Edges, -Components |
List of components (each a vertex list) |
topological_sort(Edges, Order) |
+Edges, -Order |
Topological ordering of a DAG; fails if cyclic |
has_cycle(Edges) |
+Edges |
Succeeds if the directed graph contains a cycle |
connected_components([['a', 'b'], ['c', 'd']], C)
# C = [['a', 'b'], ['c', 'd']]
topological_sort([['a', 'b'], ['b', 'c'], ['a', 'c']], O)
# O = ['a', 'b', 'c']
Tree predicates
| Predicate |
Mode |
Description |
spanning_tree(Edges, Tree) |
+Edges, -Tree |
A spanning tree (edge subset) via BFS |
min_spanning_tree(Edges, Tree, Cost) |
+Edges, -Tree, -Cost |
Minimum spanning tree via Prim's algorithm |
min_spanning_tree([['a', 'b', 1], ['b', 'c', 2], ['a', 'c', 4]], T, C)
# T = [['a', 'b', 1], ['b', 'c', 2]] C = 3
| Predicate |
Mode |
Description |
reverse_edges(Edges, Reversed) |
+Edges, -Reversed |
reverse all edge directions |
merge_graphs(Edges1, Edges2, Merged) |
+Edges1, +Edges2, -Merged |
union of two edge lists |
reverse_edges([['a', 'b'], ['c', 'd']], R)
# R = [['b', 'a'], ['d', 'c']]
See also: Tabling — memoised search for cycle-free results on cyclic graphs · Lists — list predicates used by graph algorithms.