Your first graph
Getting started resolved one relation over four nodes. This
page does the same three things with a graph big enough to be worth asking about: declare
the nodes, relate them, read the answer back — still one library, gen-graph, still no
hub.
-
Declare the nodes
Section titled “Declare the nodes”Nodes are just your data. Nothing here is gen vocabulary —
typeis a field you invented, not a keyword gen-graph knows about:services = {web = { deps = [ "api" ]; type = "frontend"; };api = { deps = [ "db" "cache" ]; type = "backend"; };worker = { deps = [ "db" "queue" ]; type = "backend"; };db = { deps = [ ]; type = "datastore"; };cache = { deps = [ ]; type = "datastore"; };queue = { deps = [ ]; type = "datastore"; };}; -
Relate them
Section titled “Relate them”The relation is the
depsfield, and an accessor record is how you tell gen-graph to read it.edgesis the only field a traversal needs;nodeDatais what lets a query read a node’s other attributes back out:g = {edges = id: services.${id}.deps or [ ];parent = _: null;nodes = builtins.attrNames services;nodeData = id: services.${id};}; -
Read the result back
Section titled “Read the result back”{entryPoints = graph.roots g; # nothing depends on thesedatastores = graph.leaves g; # these depend on nothingwebDeps = graph.reachableFrom g "web"; # everything "web" needs, transitivelydbImpact = graph.dependents g "db"; # everything that needs "db"backendNodes = graph.select g (d: d.type == "backend"); # a query over nodeData, not depshasCycles = graph.cycles g != [ ];}Evaluated against
gen-graphat HEAD, this returns:{"entryPoints": ["web", "worker"],"datastores": ["cache", "db", "queue"],"webDeps": ["api", "db", "cache"],"dbImpact": ["api", "web", "worker"],"backendNodes": ["api", "worker"],"hasCycles": false}Only membership is a contract worth relying on — the order of
webDepsanddbImpactis whatever the traversal visits first, not something to build logic on.
A second relation, for free
Section titled “A second relation, for free”g above declares parent = _: null; — every node relates to nothing by containment.
Give it a real containment relation instead and the same accessor record answers a
different kind of question, ancestorsOf instead of reachableFrom:
hosts = { "env:prod" = { parent = null; }; "host:web" = { parent = "env:prod"; }; "host:db" = { parent = "env:prod"; };};
g2 = { edges = _: [ ]; parent = id: hosts.${id}.parent or null; nodes = builtins.attrNames hosts; nodeData = id: hosts.${id};};
graph.ancestorsOf g2 "host:web" # [ "env:prod" ]edges and parent are two different relations over the same node set, read by two
different accessors. Nothing stops a real graph from declaring both, plus a third for
whatever else your data relates by.
The same model, at any scale
Section titled “The same model, at any scale”This is the graph the rest of gen builds on, not a simplified stand-in for it. A
framework’s hosts, users and aspects are nodes exactly like services above; what a
framework adds is more relations (declared, and some produced by policies rather than
written by hand) and a vocabulary of its own. The graph model
covers what a node and an edge are in gen’s own terms; Policies
covers the ones a program produces instead of you writing them.
Going further
Section titled “Going further”gen-graph’s structural combinators (overlay, connect, the edge-map algebra) are
informed by Mokhov’s algebraic graph construction — see our reading
if you want the theory behind them.
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).