This module contains the primitives to build, update and traverse graphs. If you find that this module is hard to use or the documentation is insufficient, consider opening an issue for that (and possibly even a pull request :)).
Internally, we use the elm-intdict
package for efficient dynamic graph
representation.
Basics.Int
The type used for identifying nodes, an integer.
{ id : NodeId, label : n }
The type representing a node: An identifier with a label.
{ from : NodeId
, to : NodeId
, label : e
}
Represents a directd edge in the graph. In addition to start and end node identifiers, a label value can be attached to an edge.
IntDict e
Adjacency is represented as an ordered dictionary rather than as an ordered list. This enables more dynamic graphs with efficient edge removal and insertion on the run.
{ node : Node n
, incoming : Adjacency e
, outgoing : Adjacency e
}
Represents a node with its incoming and outgoing edges (predecessors and successors).
The central graph type. It is parameterized both over the node label type n
and the edge label type e
.
One can build such a graph with the primitives under Build. Most of the time
fromNodesAndEdges
works fairly well.
For simplicity, this library just uses a patricia trie based graph representation, which means
it is just an efficient version of Dict NodeId (NodeContext n e)
. This allows efficient insertion and
removal of nodes of the graph after building.
empty : Graph n e
An empty graph.
size empty == 0
update : NodeId -> (Maybe (NodeContext n e) -> Maybe (NodeContext n e)) -> Graph n e -> Graph n e
Analogous to Dict.update
, update nodeId updater graph
will find
the node context of the node with id nodeId
in graph
. It will then call updater
with Just
that node context if that node was found and Nothing
otherwise. updater
can then return Just
an updated node context
(modifying edges is also permitted!) or delete the node by returning
Nothing
. The updated graph
is returned.
This is the most powerful building function since all possible per-node operations are possible (node removal, insertion and updating of context properties).
The other operations can be implemented in terms of update
like this:
remove nodeId graph =
update nodeId (always Nothing) graph
insert nodeContext graph =
update nodeContext.node.id (always (Just nodeContext)) graph
insert : NodeContext n e -> Graph n e -> Graph n e
Analogous to Dict.insert
, insert nodeContext graph
inserts a fresh node
with its context (label, id and edges) into graph
. If there was already a node
with the same id, it will be replaced by the new node context.
graph1 = fromNodesAndEdges [Node 1 "1"] []
newNode =
{ node = Node 2 "2"
, incoming = IntDict.singleton 1 () -- so there will be an edge from 1 to 2
, outgoing = IntDict.empty
}
graph2 = insert newNode graph1
size graph2 == 2
It's possible to build up whole graphs this way, but a lot less tedious way would
be simply to use fromNodesAndEdges
.
remove : NodeId -> Graph n e -> Graph n e
Analogous to Dict.remove
, remove nodeId graph
returns a version of graph
without a node with id nodeId
. If there was no node with that id, then remove
is a no-op:
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] [Edge 1 2 ()]
graph == remove 42 graph
graph |> remove 2 |> size == 1
inducedSubgraph : List NodeId -> Graph n e -> Graph n e
The induced subgraph of a number of node ids.
isEmpty : Graph n e -> Basics.Bool
isEmpty graph
is true if and only if there are no nodes in the graph.
Some properties to reason about in code, which hold for any graph
:
isEmpty graph =
graph == empty
isEmpty graph =
size graph == 0
size : Graph n e -> Basics.Int
size graph
returns the number of nodes in graph
.
size empty == 0
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] []
size graph == 2
member : NodeId -> Graph n e -> Basics.Bool
Analogous to Dict.member
, member nodeId graph
is true, if and only if
there is a node with id nodeId
in graph
.
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] []
member 42 graph == False
member 1 graph == True
get : NodeId -> Graph n e -> Maybe (NodeContext n e)
Analogous to Dict.get
, get nodeId graph
returns the Just
the node
context with id nodeId
in graph
if there is one and Nothing
otherwise.
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] []
get 42 graph == Nothing
get 1 graph == Just <node context of node 1>
nodeIdRange : Graph n e -> Maybe ( NodeId, NodeId )
nodeIdRange graph
returns Just (minNodeId, maxNodeId)
if graph
is not empty and Nothing
otherwise.
This is useful for finding unoccupied node ids without trial and error.
nodeIdRange empty == Nothing
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] []
nodeIdRange graph == Just (1, 2)
nodeIds : Graph n e -> List NodeId
nodeIds graph
returns a list of all nodes' ids in graph
.
nodeIds empty == []
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] []
nodeIds graph == [1, 2]
nodes : Graph n e -> List (Node n)
nodes graph
returns a list of all Node
s (e.g. id
and label
) in
graph
.
nodes empty == []
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] []
nodes graph == [Node 1 "1", Node 2 "2"]
edges : Graph n e -> List (Edge e)
edges graph
returns a list of all Edge
s (e.g. a record of from
and to
ids
and a label
) in graph
.
edges empty == []
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] [Edge 1 2 "->"]
edges graph == [Edge 1 2 "->"]
fromNodesAndEdges : List (Node n) -> List (Edge e) -> Graph n e
fromNodesAndEdges nodes edges
constructs a graph from the supplied nodes
and edges
. This is the most comfortable way to construct a graph as a whole.
Oftentimes it is even more convenient to use fromNodeLabelsAndEdgePairs
when
edges are unlabeled anyway and auto incremented node ids are OK.
The following constructs a graph with 2 nodes with a string label, connected by an edge labeled "->".
graph =
fromNodesAndEdges [ Node 1 "1", Node 2 "2" ] [ Edge 1 2 "->" ]
fromNodeLabelsAndEdgePairs : List n -> List ( NodeId, NodeId ) -> Graph n ()
A more convenient version of fromNodesAndEdges
, when edges are unlabeled
and there are no special requirements on node ids.
fromNodeLabelsAndEdgePairs labels edges
implicitly assigns node ids according
to the label's index in labels
and the list of edge pairs is converted to
unlabeled Edge
s.
graph =
fromNodeLabelsAndEdgePairs [ 'a', 'b' ] [ ( 0, 1 ) ]
fold : (NodeContext n e -> acc -> acc) -> acc -> Graph n e -> acc
A fold over all node contexts. The accumulated value is computed lazily, so that the fold can exit early when the suspended accumulator is not forced.
hasLoop ctx = IntDict.member ctx.node.id ctx.incoming
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] [Edge 1 2 "->"]
-- The graph should not have any loop.
fold (\ctx acc -> acc || hasLoop ctx) False graph == False
mapContexts : (NodeContext n1 e1 -> NodeContext n2 e2) -> Graph n1 e1 -> Graph n2 e2
Maps each node context to another one. This may change edge and node labels (including their types), possibly the node ids and also add or remove edges entirely through modifying the adjacency lists.
The following is a specification for reverseEdges:
flipEdges ctx = { ctx | incoming = ctx.outgoing, outgoing = ctx.incoming }
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] [Edge 1 2 "->"]
reverseEdges graph == mapContexts flipEdges graph
mapNodes : (n1 -> n2) -> Graph n1 e -> Graph n2 e
Maps over node labels, possibly changing their types. Leaves the graph topology intact.
mapEdges : (e1 -> e2) -> Graph n e1 -> Graph n e2
Maps over edge labels, possibly chaing their types. Leaves the graph topology intact.
reverseEdges : Graph n e -> Graph n e
Reverses the direction of every edge in the graph.
symmetricClosure : (NodeId -> NodeId -> e -> e -> e) -> Graph n e -> Graph n e
symmetricClosure edgeMerger graph
is the
symmetric closure of graph
,
e.g. the undirected equivalent, where for every edge in graph
there is also
a corresponding reverse edge. This implies that ctx.incoming
== ctx.outgoing
for each node context ctx
.
edgeMerger
resolves conflicts for when there are already edges in both
directions, e.g. the graph isn't truly directed. It is guaranteed that
edgeMerger
will only be called with the smaller node id passed in first
to enforce consitency of merging decisions.
graph = fromNodesAndEdges [Node 1 "1", Node 2 "2"] [Edge 1 2 "->"]
onlyUndirectedEdges ctx =
ctx.incoming == ctx.outgoing
merger from to outgoingLabel incomingLabel =
outgoingLabel -- quite arbitrary, will not be called for the above graph
fold
(\ctx acc -> acc && onlyUndirectedEdges ctx)
True
(symmetricClosure merger graph)
== True
AcyclicGraph
wraps a Graph
and witnesses the fact that
it is acyclic.
This can be passed on to functions that only work on acyclic graphs,
like topologicalSort
and heightLevels
.
checkAcyclic : Graph n e -> Result (Edge e) (AcyclicGraph n e)
checkAcyclic graph
checks graph
for cycles.
If there are any cycles, this will return Err edge
,
where edge
is an Edge
that is part of a cycle.
If there aren't any cycles, this will return Ok acyclic
, where
acyclic
is an AcyclicGraph
that witnesses this fact.
NodeContext n e -> List NodeId
Selects the next neighbors for the currently visited node in the traversal.
alongOutgoingEdges : NodeContext n e -> List NodeId
A good default for selecting neighbors is to just go along outgoing edges:
alongOutgoingEdges ctx =
IntDict.keys ctx.outgoing
dfs
/bfs
use this as their selecting strategy.
alongIncomingEdges : NodeContext n e -> List NodeId
A less common way for selecting neighbors is to follow incoming edges:
alongIncomingEdges ctx =
IntDict.keys ctx.incoming
NodeContext n e -> acc -> acc
A generic node visitor just like that in the ordinary fold
function.
There are combinators that make these usable for both depth-first traversal
(onDiscovery
, onFinish
) and breadth-first traversal (ignorePath
).
NodeContext n e -> acc -> ( acc
, acc -> acc
}
A node visitor specialized for depth-first traversal. Along with the node context of the currently visited node, the current accumulated value is passed. The visitor then has the chance to both modify the value at discovery of the node through the first return value and also provide a finishing transformation which is called with the value after all children were processed and the node is about to be finished.
In the cases where you don't need access to the value both at dicovery and at
finish, look into onDiscovery
and onFinish
.
onDiscovery : SimpleNodeVisitor n e acc -> NodeContext n e -> acc -> ( acc, acc -> acc )
Transform a SimpleNodeVisitor
into an equivalent DfsNodeVisitor
, which
will be called upon node discovery. This eases providing DfsNodeVisitor
s in
the default case:
dfsPreOrder : Graph n e -> List (NodeContext n e)
dfsPreOrder graph =
List.reverse (dfs (onDiscovery (::)) [] graph)
onFinish : SimpleNodeVisitor n e acc -> NodeContext n e -> acc -> ( acc, acc -> acc )
Transform a SimpleNodeVisitor
into an equivalent DfsNodeVisitor
, which
will be called upon node finish. This eases providing DfsNodeVisitor
s in
the default case:
dfsPostOrder : Graph n e -> List (NodeContext n e)
dfsPostOrder graph =
List.reverse (dfs (onFinish (::)) [] graph)
dfs : DfsNodeVisitor n e acc -> acc -> Graph n e -> acc
An off-the-shelf depth-first traversal. It will visit all components of the
graph in no guaranteed order, discovering nodes alongOutgoingEdges
.
See the docs of DfsNodeVisitor
on how to supply such a beast. There are also
examples on how to use dfs
.
dfsTree : NodeId -> Graph n e -> Tree (NodeContext n e)
dfsTree seed graph
computes a depth-first spanning tree of the component
in graph
starting from seed
alongOutgoingEdges
. This function is exemplary for needing to
utilize the whole power of DfsNodeVisitor
.
dfsForest : List NodeId -> Graph n e -> Tree.Forest (NodeContext n e)
dfsForest seeds graph
computes a depth-first spanning Forest
of the
components in graph
spanned by seeds
alongOutgoingEdges
.
A traversal over this forest would be equivalent to a depth-first traversal over the original graph.
guidedDfs : NeighborSelector n e -> DfsNodeVisitor n e acc -> List NodeId -> acc -> Graph n e -> ( acc, Graph n e )
The dfs*
functions are not powerful enough? Go for this beast.
guidedDfs selectNeighbors visitNode seeds acc graph
will perform a depth-first
traversal on graph
starting with a stack of seeds
. The children of each node
will be selected with selectNeighbors
(see NeighborSelector
), the visiting
of nodes is handled by visitNode
(c.f. DfsNodeVisitor
), folding acc
over
the graph.
When there are not any more nodes to be visited, the function will return the
accumulated value together with the unvisited rest of graph
.
dfsPreOrder graph =
-- NodeId 1 is just a wild guess here
guidedDfs alongOutgoingEdges (onDiscovery (::)) [ 1 ] [] graph
List (NodeContext n e) -> Basics.Int -> acc -> acc
A specialized node visitor for breadth-first traversal. Compared to a
SimpleNodeVisitor
, the path of contexts from the root to the current
node is passed instead of just the current node's context. Additionally, the
distance from the root is passed as an Int
(the root has distance 0 and it
holds always that length path == distance - 1
).
If you don't need the additional information, you can turn a SimpleNodeVisitor
into a BfsNodeVisitor
by calling ignorePath
.
ignorePath : SimpleNodeVisitor n e acc -> List (NodeContext n e) -> Basics.Int -> acc -> acc
Turns a SimpleNodeVisitor
into a BfsNodeVisitor
by ignoring the path
and distance parameters.
This is useful for when the visitor should be agnostic of the
traversal (breadth-first or depth-first or even just fold
).
bfsLevelOrder : List (NodeContext n e)
bfsLevelOrder graph =
graph
|> bfs (ignorePath (::)) []
|> List.reverse
bfs : BfsNodeVisitor n e acc -> acc -> Graph n e -> acc
An off-the-shelf breadth-first traversal. It will visit all components of the
graph in no guaranteed order, discovering nodes alongOutgoingEdges
.
See the docs of BfsNodeVisitor
on how to supply such a beast. There are also
examples on how to use bfs
.
guidedBfs : NeighborSelector n e -> BfsNodeVisitor n e acc -> List NodeId -> acc -> Graph n e -> ( acc, Graph n e )
The bfs
function is not powerful enough? Go for this beast.
guidedBfs selectNeighbors visitNode seeds acc graph
will perform a breadth-first
traversal on graph
starting with a queue of seeds
. The children of each node
will be selected with selectNeighbors
(see NeighborSelector
), the visiting
of nodes is handled by visitNode
(c.f. BfsNodeVisitor
), folding acc
over
the graph.
When there are not any more nodes to be visited, the function will return the
accumulated value together with the unvisited rest of graph
.
bfsLevelOrder graph =
-- NodeId 1 is just a wild guess here
guidedBfs alongOutgoingEdges (ignorePath (::)) [ 1 ] [] graph
topologicalSort : AcyclicGraph n e -> List (NodeContext n e)
Computes a
topological ordering
of the given AcyclicGraph
.
heightLevels : AcyclicGraph n e -> List (List (NodeContext n e))
Computes the height function of a given AcyclicGraph
. This is a more general
topological sort,
where independent nodes are in the same height level (e.g. the same list
index). A valid topological sort is trivially obtained by flattening the
result of this function.
The height function is useful for solving the maximal clique problem for certain perfect graphs (comparability graphs). There is the excellent reference Algorithmic Graph Theory and Perfect Graphs.
stronglyConnectedComponents : Graph n e -> Result (List (Graph n e)) (AcyclicGraph n e)
Decomposes a graph into its strongly connected components.
Ok acyclic
means that the graph was acyclic (so every node in the
graph forms a single connected component).
Err components
means there were cycles in the graph. The resulting
list of components
is a topological ordering of the condensation (e.g. the
acyclic component graph) of the input graph.
toString : (n -> Maybe String) -> (e -> Maybe String) -> Graph n e -> String
Returns a string representation of the graph.