graph: refactor Graph into being an interface

This commit is contained in:
Brian Picciano 2018-10-04 17:31:40 -04:00
parent 16f2b1bbde
commit ad1e99585b
2 changed files with 198 additions and 124 deletions

View File

@ -63,6 +63,25 @@ func (e edge) Head() Value {
return e.head return e.head
} }
func (e edge) String() string {
return edgeID(e)
}
// NOTE the Node type exists primarily for convenience. As far as Graph's
// internals are concerned it doesn't _really_ exist, and no Graph method should
// ever take Node as a parameter (except the callback functions like in
// Traverse, where it's not really being taken in).
// Node wraps a Value in a Graph to include that Node's input and output Edges
// in that Graph.
type Node struct {
Value
// All Edges in the Graph with this Node's Value as their Head and Tail,
// respectively. These should not be expected to be deterministic.
Ins, Outs []Edge
}
// an edgeIndex maps valueIDs to a set of edgeIDs. Graph keeps two edgeIndex's, // an edgeIndex maps valueIDs to a set of edgeIDs. Graph keeps two edgeIndex's,
// one for input edges and one for output edges. // one for input edges and one for output edges.
type edgeIndex map[string]map[string]struct{} type edgeIndex map[string]map[string]struct{}
@ -111,7 +130,36 @@ func (ei edgeIndex) del(valID, edgeID string) {
// //
// The Graph does not keep track of Edge ordering. Assume that all slices of // The Graph does not keep track of Edge ordering. Assume that all slices of
// Edges are in random order. // Edges are in random order.
type Graph struct { type Graph interface {
// Empty returns a graph with no edges which is of the same underlying type
// as this one.
Empty() Graph
// Add returns a new Graph instance with the given Edge added to it. If the
// original Graph already had that Edge this returns the original Graph.
Add(Edge) Graph
// Del returns a new Graph instance without the given Edge in it. If the
// original Graph didn't have that Edge this returns the original Graph.
Del(Edge) Graph
// Edges returns all Edges which are part of the Graph, mapped using a
// string ID which is unique within the Graph and between Graphs of the same
// underlying type.
Edges() map[string]Edge
// EdgesTo returns all Edges whose Head is the given Value.
EdgesTo(v Value) []Edge
// EdgesFrom returns all Edges whose Tail is the given Value.
EdgesFrom(v Value) []Edge
// Has returns true if the Graph contains at least one Edge with a Head or
// Tail of Value.
Has(v Value) bool
}
type graph struct {
m map[string]Edge m map[string]Edge
// these are indices mapping Value IDs to all the in/out edges for that // these are indices mapping Value IDs to all the in/out edges for that
@ -119,8 +167,15 @@ type Graph struct {
vIns, vOuts edgeIndex vIns, vOuts edgeIndex
} }
func (g Graph) cp() Graph { // Null is the empty graph from which all other Graphs are built.
g2 := Graph{ var Null = (graph{}).Empty()
func (g graph) Empty() Graph {
return (graph{}).cp() // cp also initializes
}
func (g graph) cp() graph {
g2 := graph{
m: make(map[string]Edge, len(g.m)), m: make(map[string]Edge, len(g.m)),
vIns: g.vIns.cp(), vIns: g.vIns.cp(),
vOuts: g.vOuts.cp(), vOuts: g.vOuts.cp(),
@ -131,18 +186,16 @@ func (g Graph) cp() Graph {
return g2 return g2
} }
func (g Graph) String() string { func (g graph) String() string {
edgeIDs := make([]string, 0, len(g.m)) edgeStrs := make([]string, 0, len(g.m))
for edgeID := range g.m { for _, edge := range g.m {
edgeIDs = append(edgeIDs, edgeID) edgeStrs = append(edgeStrs, fmt.Sprint(edge))
} }
sort.Strings(edgeIDs) sort.Strings(edgeStrs)
return "Graph{" + strings.Join(edgeIDs, ",") + "}" return "Graph{" + strings.Join(edgeStrs, ",") + "}"
} }
// Add returns a new Graph instance with the given Edge added to it. If the func (g graph) Add(e Edge) Graph {
// original Graph already had that Edge this returns the original Graph.
func (g Graph) Add(e Edge) Graph {
id := edgeID(e) id := edgeID(e)
if _, ok := g.m[id]; ok { if _, ok := g.m[id]; ok {
return g return g
@ -153,24 +206,26 @@ func (g Graph) Add(e Edge) Graph {
return g2 return g2
} }
func (g Graph) addDirty(edgeID string, e Edge) { func (g graph) addDirty(edgeID string, e Edge) {
g.m[edgeID] = e g.m[edgeID] = e
g.vIns.add(e.Head().ID, edgeID) g.vIns.add(e.Head().ID, edgeID)
g.vOuts.add(e.Tail().ID, edgeID) g.vOuts.add(e.Tail().ID, edgeID)
} }
func (g Graph) estSize() int { // addDirty attempts to add the Edge to Graph using an addDirty method,
lvIns := len(g.vIns) // otherwise it just uses Add like normal
lvOuts := len(g.vOuts) func addDirty(g Graph, edgeID string, e Edge) Graph {
if lvIns > lvOuts { gd, ok := g.(interface {
return lvIns addDirty(string, Edge)
})
if !ok {
return g.Add(e)
} }
return lvOuts gd.addDirty(edgeID, e)
return g
} }
// Del returns a new Graph instance without the given Edge in it. If the func (g graph) Del(e Edge) Graph {
// original Graph didn't have that Edge this returns the original Graph.
func (g Graph) Del(e Edge) Graph {
id := edgeID(e) id := edgeID(e)
if _, ok := g.m[id]; !ok { if _, ok := g.m[id]; !ok {
return g return g
@ -183,15 +238,50 @@ func (g Graph) Del(e Edge) Graph {
return g2 return g2
} }
func (g graph) Edges() map[string]Edge {
return g.m
}
func (g graph) EdgesTo(v Value) []Edge {
vIns := g.vIns[v.ID]
ins := make([]Edge, 0, len(vIns))
for edgeID := range vIns {
ins = append(ins, g.m[edgeID])
}
return ins
}
func (g graph) EdgesFrom(v Value) []Edge {
vOuts := g.vOuts[v.ID]
outs := make([]Edge, 0, len(vOuts))
for edgeID := range vOuts {
outs = append(outs, g.m[edgeID])
}
return outs
}
func (g graph) Has(v Value) bool {
if _, ok := g.vIns[v.ID]; ok {
return true
} else if _, ok := g.vOuts[v.ID]; ok {
return true
}
return false
}
////////////////////////////////////////////////////////////////////////////////
// Disjoin looks at the whole Graph and returns all sub-graphs of it which don't // Disjoin looks at the whole Graph and returns all sub-graphs of it which don't
// share any Edges between each other. // share any Edges between each other.
func (g Graph) Disjoin() []Graph { func Disjoin(g Graph) []Graph {
valM := make(map[string]*Graph, len(g.vOuts)) empty := g.Empty()
edges := g.Edges()
valM := make(map[string]*Graph, len(edges))
graphForEdge := func(edge Edge) *Graph { graphForEdge := func(edge Edge) *Graph {
headGraph := valM[edge.Head().ID] headGraph := valM[edge.Head().ID]
tailGraph := valM[edge.Tail().ID] tailGraph := valM[edge.Tail().ID]
if headGraph == nil && tailGraph == nil { if headGraph == nil && tailGraph == nil {
newGraph := Graph{}.cp() // cp also initializes newGraph := empty.Empty()
return &newGraph return &newGraph
} else if headGraph == nil && tailGraph != nil { } else if headGraph == nil && tailGraph != nil {
return tailGraph return tailGraph
@ -204,11 +294,13 @@ func (g Graph) Disjoin() []Graph {
// the two values are part of different graphs, join the smaller into // the two values are part of different graphs, join the smaller into
// the larger and change all values which were pointing to it to point // the larger and change all values which were pointing to it to point
// into the larger (which will then be the join of them) // into the larger (which will then be the join of them)
if len(tailGraph.m) > len(headGraph.m) { tailEdges := (*tailGraph).Edges()
tailGraph, headGraph = headGraph, tailGraph if headEdges := (*headGraph).Edges(); len(headEdges) > len(tailEdges) {
headGraph, tailGraph = tailGraph, headGraph
tailEdges = headEdges
} }
for edgeID, edge := range tailGraph.m { for edgeID, edge := range tailEdges {
(*headGraph).addDirty(edgeID, edge) *headGraph = addDirty(*headGraph, edgeID, edge)
} }
for valID, valGraph := range valM { for valID, valGraph := range valM {
if valGraph == tailGraph { if valGraph == tailGraph {
@ -218,9 +310,9 @@ func (g Graph) Disjoin() []Graph {
return headGraph return headGraph
} }
for edgeID, edge := range g.m { for edgeID, edge := range edges {
graph := graphForEdge(edge) graph := graphForEdge(edge)
(*graph).addDirty(edgeID, edge) *graph = addDirty(*graph, edgeID, edge)
valM[edge.Head().ID] = graph valM[edge.Head().ID] = graph
valM[edge.Tail().ID] = graph valM[edge.Tail().ID] = graph
} }
@ -237,60 +329,35 @@ func (g Graph) Disjoin() []Graph {
return graphs return graphs
} }
// Join returns a new Graph which shares all Edges of this Graph and all given // Join returns a new Graph which shares all Edges of all given Graphs. All
// Graphs. // given Graphs must be of the same underlying type.
func (g Graph) Join(graphs ...Graph) Graph { func Join(graphs ...Graph) Graph {
g2 := g.cp() g2 := graphs[0].Empty()
for _, graph := range graphs { for _, graph := range graphs {
for edgeID, edge := range graph.m { for edgeID, edge := range graph.Edges() {
g2.addDirty(edgeID, edge) g2 = addDirty(g2, edgeID, edge)
} }
} }
return g2 return g2
} }
// Edges returns all Edges which are part of the Graph // GetNode returns the Node for the given Value, or false if the Graph doesn't
func (g Graph) Edges() []Edge {
edges := make([]Edge, 0, len(g.m))
for _, e := range g.m {
edges = append(edges, e)
}
return edges
}
// NOTE the Node type exists primarily for convenience. As far as Graph's
// internals are concerned it doesn't _really_ exist, and no Graph method should
// ever take Node as a parameter (except the callback functions like in
// Traverse, where it's not really being taken in).
// Node wraps a Value in a Graph to include that Node's input and output Edges
// in that Graph.
type Node struct {
Value
// All Edges in the Graph with this Node's Value as their Head and Tail,
// respectively. These should not be expected to be deterministic.
Ins, Outs []Edge
}
// Node returns the Node for the given Value, or false if the Graph doesn't
// contain the Value. // contain the Value.
func (g Graph) Node(v Value) (Node, bool) { func GetNode(g Graph, v Value) (Node, bool) {
n := Node{Value: v} n := Node{
for edgeID := range g.vIns[v.ID] { Value: v,
n.Ins = append(n.Ins, g.m[edgeID]) Ins: g.EdgesTo(v),
} Outs: g.EdgesFrom(v),
for edgeID := range g.vOuts[v.ID] {
n.Outs = append(n.Outs, g.m[edgeID])
} }
return n, len(n.Ins) > 0 || len(n.Outs) > 0 return n, len(n.Ins) > 0 || len(n.Outs) > 0
} }
// Nodes returns a Node for each Value which has at least one Edge in the Graph, // GetNodes returns a Node for each Value which has at least one Edge in the
// with the Nodes mapped by their Value's ID. // Graph, with the Nodes mapped by their Value's ID.
func (g Graph) Nodes() map[string]Node { func GetNodes(g Graph) map[string]Node {
nodesM := make(map[string]Node, len(g.m)*2) edges := g.Edges()
for _, edge := range g.m { nodesM := make(map[string]Node, len(edges)*2)
for _, edge := range edges {
// if head and tail are modified at the same time it messes up the case // if head and tail are modified at the same time it messes up the case
// where they are the same node // where they are the same node
{ {
@ -311,17 +378,6 @@ func (g Graph) Nodes() map[string]Node {
return nodesM return nodesM
} }
// Has returns true if the Graph contains at least one Edge with a Head or Tail
// of Value.
func (g Graph) Has(v Value) bool {
if _, ok := g.vIns[v.ID]; ok {
return true
} else if _, ok := g.vOuts[v.ID]; ok {
return true
}
return false
}
// Traverse is used to traverse the Graph until a stopping point is reached. // Traverse is used to traverse the Graph until a stopping point is reached.
// Traversal starts with the cursor at the given start Value. Each hop is // Traversal starts with the cursor at the given start Value. Each hop is
// performed by passing the cursor Value's Node into the next function. The // performed by passing the cursor Value's Node into the next function. The
@ -332,10 +388,10 @@ func (g Graph) Has(v Value) bool {
// //
// If start has no Edges in the Graph, or a Value returned from next doesn't, // If start has no Edges in the Graph, or a Value returned from next doesn't,
// this will still call next, but the Node will be the zero value. // this will still call next, but the Node will be the zero value.
func (g Graph) Traverse(start Value, next func(n Node) (Value, bool)) { func Traverse(g Graph, start Value, next func(n Node) (Value, bool)) {
curr := start curr := start
for { for {
currNode, ok := g.Node(curr) currNode, ok := GetNode(g, curr)
if ok { if ok {
curr, ok = next(currNode) curr, ok = next(currNode)
} else { } else {
@ -355,9 +411,9 @@ func (g Graph) Traverse(start Value, next func(n Node) (Value, bool)) {
// Value has no edges in the Graph, traversal stops and this method returns. // Value has no edges in the Graph, traversal stops and this method returns.
// //
// The exact order of Nodes visited is _not_ deterministic. // The exact order of Nodes visited is _not_ deterministic.
func (g Graph) VisitBreadth(start Value, callback func(n Node) bool) { func VisitBreadth(g Graph, start Value, callback func(n Node) bool) {
visited := map[string]bool{} visited := map[string]bool{}
toVisit := make([]Value, 0, g.estSize()) toVisit := make([]Value, 0, 16)
toVisit = append(toVisit, start) toVisit = append(toVisit, start)
for { for {
@ -371,7 +427,7 @@ func (g Graph) VisitBreadth(start Value, callback func(n Node) bool) {
if visited[val.ID] { if visited[val.ID] {
continue continue
} }
node, ok := g.Node(val) node, ok := GetNode(g, val)
if !ok { if !ok {
continue continue
} else if !callback(node) { } else if !callback(node) {
@ -396,11 +452,11 @@ func (g Graph) VisitBreadth(start Value, callback func(n Node) bool) {
// Value has no edges in the Graph, traversal stops and this method returns. // Value has no edges in the Graph, traversal stops and this method returns.
// //
// The exact order of Nodes visited is _not_ deterministic. // The exact order of Nodes visited is _not_ deterministic.
func (g Graph) VisitDepth(start Value, callback func(n Node) bool) { func VisitDepth(g Graph, start Value, callback func(n Node) bool) {
// VisitDepth is actually the same as VisitBreadth, only you read off the // VisitDepth is actually the same as VisitBreadth, only you read off the
// toVisit list from back-to-front // toVisit list from back-to-front
visited := map[string]bool{} visited := map[string]bool{}
toVisit := make([]Value, 0, g.estSize()) toVisit := make([]Value, 0, 16)
toVisit = append(toVisit, start) toVisit = append(toVisit, start)
for { for {
@ -413,7 +469,7 @@ func (g Graph) VisitDepth(start Value, callback func(n Node) bool) {
if visited[val.ID] { if visited[val.ID] {
continue continue
} }
node, ok := g.Node(val) node, ok := GetNode(g, val)
if !ok { if !ok {
continue continue
} else if !callback(node) { } else if !callback(node) {
@ -429,31 +485,38 @@ func (g Graph) VisitDepth(start Value, callback func(n Node) bool) {
} }
} }
func (g Graph) edgesShared(g2 Graph) bool { func edgesShared(g, g2 Graph) bool {
for id := range g2.m { gEdges := g.Edges()
if _, ok := g.m[id]; !ok { for id := range g2.Edges() {
if _, ok := gEdges[id]; !ok {
return false return false
} }
} }
return true return true
} }
// SubGraph returns true if the given Graph shares all of its Edges with this // SubGraph returns true if g2 is a sub-graph of g; i.e., all edges in g2 are
// Graph. // also in g. Both Graphs should be of the same underlying type.
func (g Graph) SubGraph(g2 Graph) bool { func SubGraph(g, g2 Graph) bool {
gEdges, g2Edges := g.Edges(), g2.Edges()
// as a quick check before iterating through the edges, if g has fewer edges // as a quick check before iterating through the edges, if g has fewer edges
// than g2 then g2 can't possibly be a sub-graph of it // than g2 then g2 can't possibly be a sub-graph of it
if len(g.m) < len(g2.m) { if len(gEdges) < len(g2Edges) {
return false return false
} }
return g.edgesShared(g2) for id := range g2Edges {
if _, ok := gEdges[id]; !ok {
return false
}
}
return true
} }
// Equal returns true if the given Graph and this Graph have exactly the same // Equal returns true if g and g2 have exactly the same Edges. Both Graphs
// Edges. // should be of the same underlying type.
func (g Graph) Equal(g2 Graph) bool { func Equal(g, g2 Graph) bool {
if len(g.m) != len(g2.m) { if len(g.Edges()) != len(g2.Edges()) {
return false return false
} }
return g.edgesShared(g2) return SubGraph(g, g2)
} }

View File

@ -30,6 +30,7 @@ func TestGraph(t *T) {
chk := mchk.Checker{ chk := mchk.Checker{
Init: func() mchk.State { Init: func() mchk.State {
return state{ return state{
Graph: Null,
m: map[string]Edge{}, m: map[string]Edge{},
} }
}, },
@ -64,8 +65,8 @@ func TestGraph(t *T) {
delete(s.m, edgeID(p.del)) delete(s.m, edgeID(p.del))
} }
{ // test Nodes and Edges methods { // test GetNodes and Edges methods
nodes := s.Graph.Nodes() nodes := GetNodes(s.Graph)
edges := s.Graph.Edges() edges := s.Graph.Edges()
var aa []massert.Assertion var aa []massert.Assertion
vals := map[string]bool{} vals := map[string]bool{}
@ -93,12 +94,12 @@ func TestGraph(t *T) {
} }
} }
{ // test Node and Has. Nodes has already been tested so we can use { // test GetNode and Has. GetNodes has already been tested so we
// its returned Nodes as the expected ones // can use its returned Nodes as the expected ones
var aa []massert.Assertion var aa []massert.Assertion
for _, expNode := range s.Graph.Nodes() { for _, expNode := range GetNodes(s.Graph) {
var naa []massert.Assertion var naa []massert.Assertion
node, ok := s.Graph.Node(expNode.Value) node, ok := GetNode(s.Graph, expNode.Value)
naa = append(naa, massert.Equal(true, ok)) naa = append(naa, massert.Equal(true, ok))
naa = append(naa, massert.Equal(true, s.Graph.Has(expNode.Value))) naa = append(naa, massert.Equal(true, s.Graph.Has(expNode.Value)))
naa = append(naa, massert.Subset(expNode.Ins, node.Ins)) naa = append(naa, massert.Subset(expNode.Ins, node.Ins))
@ -108,7 +109,7 @@ func TestGraph(t *T) {
aa = append(aa, massert.Comment(massert.All(naa...), "v:%q", expNode.ID)) aa = append(aa, massert.Comment(massert.All(naa...), "v:%q", expNode.ID))
} }
_, ok := s.Graph.Node(strV("zz")) _, ok := GetNode(s.Graph, strV("zz"))
aa = append(aa, massert.Equal(false, ok)) aa = append(aa, massert.Equal(false, ok))
aa = append(aa, massert.Equal(false, s.Graph.Has(strV("zz")))) aa = append(aa, massert.Equal(false, s.Graph.Has(strV("zz"))))
@ -140,7 +141,12 @@ func TestSubGraphAndEqual(t *T) {
chk := mchk.Checker{ chk := mchk.Checker{
Init: func() mchk.State { Init: func() mchk.State {
return state{expEqual: true, expSubGraph: true} return state{
g1: Null,
g2: Null,
expEqual: true,
expSubGraph: true,
}
}, },
Next: func(ss mchk.State) mchk.Action { Next: func(ss mchk.State) mchk.Action {
i := mrand.Intn(10) i := mrand.Intn(10)
@ -162,11 +168,11 @@ func TestSubGraphAndEqual(t *T) {
s.expSubGraph = s.expSubGraph && p.add1 s.expSubGraph = s.expSubGraph && p.add1
s.expEqual = s.expEqual && p.add1 && p.add2 s.expEqual = s.expEqual && p.add1 && p.add2
if s.g1.SubGraph(s.g2) != s.expSubGraph { if SubGraph(s.g1, s.g2) != s.expSubGraph {
return nil, fmt.Errorf("SubGraph expected to return %v", s.expSubGraph) return nil, fmt.Errorf("SubGraph expected to return %v", s.expSubGraph)
} }
if s.g1.Equal(s.g2) != s.expEqual { if Equal(s.g1, s.g2) != s.expEqual {
return nil, fmt.Errorf("Equal expected to return %v", s.expEqual) return nil, fmt.Errorf("Equal expected to return %v", s.expEqual)
} }
@ -197,6 +203,7 @@ func TestDisjoinUnion(t *T) {
chk := mchk.Checker{ chk := mchk.Checker{
Init: func() mchk.State { Init: func() mchk.State {
return state{ return state{
g: Null,
valM: map[string][]Value{}, valM: map[string][]Value{},
disjM: map[string]Graph{}, disjM: map[string]Graph{},
} }
@ -228,23 +235,26 @@ func TestDisjoinUnion(t *T) {
s, p := ss.(state), a.Params.(params) s, p := ss.(state), a.Params.(params)
s.g = s.g.Add(p.e) s.g = s.g.Add(p.e)
s.valM[p.prefix] = append(s.valM[p.prefix], p.e.Head(), p.e.Tail()) s.valM[p.prefix] = append(s.valM[p.prefix], p.e.Head(), p.e.Tail())
if s.disjM[p.prefix] == nil {
s.disjM[p.prefix] = Null
}
s.disjM[p.prefix] = s.disjM[p.prefix].Add(p.e) s.disjM[p.prefix] = s.disjM[p.prefix].Add(p.e)
var aa []massert.Assertion var aa []massert.Assertion
// test Disjoin // test Disjoin
disj := s.g.Disjoin() disj := Disjoin(s.g)
for prefix, graph := range s.disjM { for prefix, graph := range s.disjM {
aa = append(aa, massert.Comment( aa = append(aa, massert.Comment(
massert.Equal(true, graph.Equal(s.disjM[prefix])), massert.Equal(true, Equal(graph, s.disjM[prefix])),
"prefix:%q", prefix, "prefix:%q", prefix,
)) ))
} }
aa = append(aa, massert.Len(disj, len(s.disjM))) aa = append(aa, massert.Len(disj, len(s.disjM)))
// now test Join // now test Join
join := (Graph{}).Join(disj...) join := Join(disj...)
aa = append(aa, massert.Equal(true, s.g.Equal(join))) aa = append(aa, massert.Equal(true, Equal(s.g, join)))
return s, massert.All(aa...).Assert() return s, massert.All(aa...).Assert()
}, },
@ -303,9 +313,10 @@ func TestVisitBreadth(t *T) {
chk := mchk.Checker{ chk := mchk.Checker{
Init: func() mchk.State { Init: func() mchk.State {
return state{ return state{
g: Null,
ranks: []map[string]bool{ ranks: []map[string]bool{
map[string]bool{"start": true}, {"start": true},
map[string]bool{}, {},
}, },
} }
}, },
@ -340,7 +351,7 @@ func TestVisitBreadth(t *T) {
var err error var err error
expRanks := s.ranks expRanks := s.ranks
currRank := map[string]bool{} currRank := map[string]bool{}
s.g.VisitBreadth(strV("start"), func(n Node) bool { VisitBreadth(s.g, strV("start"), func(n Node) bool {
currRank[n.Value.ID] = true currRank[n.Value.ID] = true
if len(currRank) != len(expRanks[0]) { if len(currRank) != len(expRanks[0]) {
return true return true