elm-community / graph / Graph

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.

Data


type alias NodeId =
Basics.Int

The type used for identifying nodes, an integer.


type alias Node n =
{ id : NodeId, label : n }

The type representing a node: An identifier with a label.


type alias Edge e =
{ 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.


type alias Adjacency e =
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.


type alias NodeContext n e =
{ node : Node n
, incoming : Adjacency e
, outgoing : Adjacency e 
}

Represents a node with its incoming and outgoing edges (predecessors and successors).


type Graph n e

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.

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.

Query

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)

List representations

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 Nodes (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 Edges (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 Edges.

graph =
    fromNodeLabelsAndEdgePairs [ 'a', 'b' ] [ ( 0, 1 ) ]

Transforms

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

Characterization


type AcyclicGraph n e

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.

Traversals

Neighbor selectors and node visitors


type alias NeighborSelector n e =
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


type alias SimpleNodeVisitor n e acc =
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).

Depth-first


type alias DfsNodeVisitor n e acc =
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 DfsNodeVisitors 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 DfsNodeVisitors 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

Breadth-first


type alias BfsNodeVisitor n e acc =
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

Topological Sort

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.

Strongly Connected Components

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.

String representation

toString : (n -> Maybe String) -> (e -> Maybe String) -> Graph n e -> String

Returns a string representation of the graph.