It\'s easy enough to represent a tree or list in haskell using algebraic data types. But how would you go about typographically representing a graph? It seems that you need
I also find it awkward to try to represent data structures with cycles in a pure language. It's the cycles that are really the problem; because values can be shared any ADT that can contain a member of the type (including lists and trees) is really a DAG (Directed Acyclic Graph). The fundamental issue is that if you have values A and B, with A containing B and B containing A, then neither can be created before the other exists. Because Haskell is lazy you can use a trick known as Tying the Knot to get around this, but that makes my brain hurt (because I haven't done much of it yet). I've done more of my substantial programming in Mercury than Haskell so far, and Mercury is strict so knot-tying doesn't help.
Usually when I've run into this before I've just resorted to additional indirection, as you're suggesting; often by using a map from ids to the actual elements, and having elements contain references to the ids instead of to other elements. The main thing I didn't like about doing that (aside from the obvious inefficiency) is that it felt more fragile, introducing the possible errors of looking up an id that doesn't exist or trying to assign the same id to more than one element. You can write code so that these errors won't occur, of course, and even hide it behind abstractions so that the only places where such errors could occur are bounded. But it's still one more thing to get wrong.
However, a quick google for "Haskell graph" led me to http://www.haskell.org/haskellwiki/The_Monad.Reader/Issue5/Practical_Graph_Handling, which looks like a worthwhile read.
I always liked Martin Erwig's approach in "Inductive Graphs and Functional Graph Algorithms", which you can read here. FWIW, I once wrote a Scala implementation as well, see https://github.com/nicolast/scalagraphs.
In shang's answer you can see how to represent a graph using laziness. The problem with these representations is that they are very difficult to change. The knot-tying trick is useful only if you're going to build a graph once, and afterward it never changes.
In practice, should I actually want to do something with my graph, I use the more pedestrian representations:
If you're going to be changing or editing the graph frequently, I recommend using a representation based on Huet's zipper. This is the representation used internally in GHC for control-flow graphs. You can read about it here:
An Applicative Control-Flow Graph based on Huet's Zipper
Hoopl: A Modular, Reusable Library for Dataflow Analysis and Transformation
I like this implementation of a graph taken from here
import Data.Maybe
import Data.Array
class Enum b => Graph a b | a -> b where
vertices :: a -> [b]
edge :: a -> b -> b -> Maybe Double
fromInt :: a -> Int -> b
It's true, graphs are not algebraic. To deal with this problem, you have a couple of options:
Int
s) and referring to them indirectly rather than algebraically. This can be made significantly more convenient by making the type abstract and providing an interface that juggles the indirection for you. This is the approach taken by, e.g., fgl and other practical graph libraries on Hackage.So there are pros and cons to each of the above choices. Pick the one that seems best for you.
A few others have briefly mentioned fgl
and Martin Erwig's Inductive Graphs and Functional Graph Algorithms, but it's probably worth writing up an answer that actually gives a sense of the data types behind the inductive representation approach.
In his paper, Erwig presents the following types:
type Node = Int
type Adj b = [(b, Node)]
type Context a b = (Adj b, Node, a, Adj b)
data Graph a b = Empty | Context a b & Graph a b
(The representation in fgl
is slightly different, and makes good use of typeclasses - but the idea is essentially the same.)
Erwig is describing a multigraph in which nodes and edges have labels, and in which all edges are directed. A Node
has a label of some type a
; an edge has a label of some type b
. A Context
is simply (1) a list of labeled edges pointing to a particular node, (2) the node in question, (3) the node's label, and (4) the list of labeled edges pointing from the node. A Graph
can then be conceived of inductively as either Empty
, or as a Context
merged (with &
) into an existing Graph
.
As Erwig notes, we can't freely generate a Graph
with Empty
and &
, as we might generate a list with the Cons
and Nil
constructors, or a Tree
with Leaf
and Branch
. Too, unlike lists (as others have mentioned), there's not going to be any canonical representation of a Graph
. These are crucial differences.
Nonetheless, what makes this representation so powerful, and so similar to the typical Haskell representations of lists and trees, is that the Graph
datatype here is inductively defined. The fact that a list is inductively defined is what allows us to so succinctly pattern match on it, process a single element, and recursively process the rest of the list; equally, Erwig's inductive representation allows us to recursively process a graph one Context
at a time. This representation of a graph lends itself to a simple definition of a way to map over a graph (gmap
), as well as a way to perform unordered folds over graphs (ufold
).
The other comments on this page are great. The main reason I wrote this answer, however, is that when I read phrases such as "graphs are not algebraic," I fear that some readers will inevitably come away with the (erroneous) impression that no one's found a nice way to represent graphs in Haskell in a way that permits pattern matching on them, mapping over them, folding them, or generally doing the sort of cool, functional stuff that we're used to doing with lists and trees.