Graphen Mathematisch sind Graphen definiert als: G = (V, E) mit V Menge von Knoten, E ⊆ V × V Menge von Kanten Wie können Graphen nun in Haskell implementiert werden? Die Datentypen leiten wir direkt aus der mathematischen Definition ab, zusätzlich fügen wir noch Beschriftungen zu Knoten und Kanten hinzu: module Graph where type type type type Graph a b = (Nodes a, Edges b) NodeId = Int Nodes a = [(NodeId, a)] Edges b = [(NodeId, b, NodeId)] Wir stellen zunächst Überlegungen zur Schnittstelle an, um die Implementierung und Effizienzbetrachtungen kümmern wir uns später. Viele (imperative) Graphalgorithmen arbeiten mit Markierungen. Ähnliches wäre auch in unserem Framework über Beschriftungen möglich, z.B. durch Erweiterung um eine boolesche Komponente. Allerdings entspricht dies nicht dem üblichen induktiven Programmieren in funktionalen Sprachen. Schöner wäre eine induktive Darstellung der Graphen, wie: • leerer Graph (Konstruktor emptyGraph) • Graph, der aus einem Knoten (mit seinem Kontext, den ein- und ausgehenden Kanten) und einem Restgraph besteht (Konstruktor &v, wobei v die zugehörige Knotennummer ist) Mit dieser Darstellung ließe sich eine Tiefensuche wie folgt implementieren (in Pseudocode): dfs dfs dfs dfs :: [NodeId] -> Graph a b -> [NodeId] [] _ = [] (v:vs) (c &v g) = v : dfs (succs c ++ vs) g (_:vs) g = dfs vs g Problematisch ist natürlich das doppelte Vorkommen von v in den Pattern (NichtLinearität) und der parametrisierte Konstruktor &. Als Lösung kann dieses Matching mit Hilfe einer Funktion umgesetzt werden: type Context a b = ([(NodeId, b)], a, [(NodeId, b)]) match :: NodeId -> Graph a b -> Maybe (Context a b, -- Kontext 1 Graph a b) -- Restgraph dfs :: [NodeId] -> Graph a b -> [NodeId] dfs [] _ = [] dfs (v:vs) g = case match v g of Nothing -> dfs vs g Just ((_ ,_ ,succs), g’) -> v : dfs (map fst succs ++ vs) g’ Es fehlt noch die Definition der Funktion match, die wir durch Suchen des Knotens und Aufsammeln der Vorgänger- und Nachfolgerknoten implementieren. match n (nodes, edges) = do a <- lookup n nodes let ctxt = ( [(m, b) | (m, b, n’) <- edges, n’ == n] , a , [(m, b) | (n’, b, m) <- edges, n’ == n] ) grph = ( filter ((/= n) . fst) nodes , [ e | e@(m, _, m’) <- edges, m /= n, m’ /= n] ) return (ctxt, grph) Für die Konstruktion von Graphen definieren wir einige Funktionen: addNode :: NodeId -> a -> Graph a b -> Graph a b addNode n a (nodes, edges) = maybe ((n, a) : nodes, edges) (error $ "Node " ++ show n ++ "already in graph") (lookup n nodes) addEdge :: NodeId -> b -> NodeId -> Graph a b -> Graph a b addEdge n b m (nodes, edges) = maybe (errNode n) (\_ -> maybe (errNode m) (const (nodes, (n, b, m) : edges)) (lookup m nodes)) (lookup n nodes) where errNode n = error $ "Node " ++ show n ++ " not in graph" addNodeWithSuccs :: NodeId -> a -> [(NodeId, b)] -> Graph a b -> Graph a b addNodeWithSuccs n a succs = foldr (.) (addNode n a) [addEdge n b m | (m, b) <- succs] Beachte: Knoten / Kanten können nun mittels match in anderer Reihenfolge entnommen werden als sie hinzugefügt wurden. In unserer Implementierung soll es mehr auf die Idee der Schnittstelle ankommen, die Repräsentation eines Graphen ist so noch ineffizient. Eine effizientere Darstellung ist 2 beispielsweise möglich unter Verwendung von Braunbäumen oder höhenbalancierten Suchbäumen, die NodeIds auf Vorgänger-/Nachfolgerknoten abbilden. Hierdurch wird die Implementierung von match komplizierter, ist aber effizient möglich. Dadurch wird das Hinzufügen von Knoten/Kanten und das Matchen logarithmisch in der Graphgröße. Eine weitere Verbesserungsmöglichkeit: Die NodeIds sind nicht abstrakt, sondern müssen durch die Anwendung (und damit den Programmierer) generiert werden. Dies lässt sich durch eine monadische Erweiterung der Graph-Konstruktion um einen NodeId-Zustand verbessern, sodass Graphen wie folgt konstruiert werden können: g = do n <- addNode "a" m <- addNode "b" addEdge n 42 m 3