diff --git a/graph/graph.go b/graph/graph.go new file mode 100644 index 0000000..b225c92 --- /dev/null +++ b/graph/graph.go @@ -0,0 +1,156 @@ +// Package graph implements an immutable unidirectional graph. +package graph + +import ( + "crypto/rand" + "encoding/hex" + "fmt" +) + +// Value wraps a go value in a way such that it will be uniquely identified +// within any Graph and between Graphs. Use NewValue to create a Value instance. +// You can create an instance manually as long as ID is globally unique. +type Value struct { + ID string + V interface{} +} + +// Void is the absence of any value. +var Void Value + +// NewValue returns a Value instance wrapping any go value. The Value returned +// will be independent of the passed in go value. So if the same go value is +// passed in twice then the two returned Value instances will be treated as +// being different values by Graph. +func NewValue(V interface{}) Value { + b := make([]byte, 8) + if _, err := rand.Read(b); err != nil { + panic(err) + } + return Value{ + ID: hex.EncodeToString(b), + V: V, + } +} + +// Edge is a directional edge connecting two values in a Graph, the Tail and the +// Head. An Edge may also contain a value of its own. +type Edge struct { + Tail, Val, Head Value +} + +func (e Edge) id() string { + return fmt.Sprintf("%q-%q->%q", e.Tail, e.Val, e.Head) +} + +// Graph implements an immutable, unidirectional graph which can hold generic +// values. All methods are thread-safe as they don't modify the Graph in any +// way. +// +// The Graph's zero value is the initial empty graph. +// +// The Graph does not keep track of Edge ordering. Assume that all slices of +// Edges are in random order. +type Graph struct { + m map[string]Edge +} + +func (g Graph) cp() Graph { + g2 := Graph{ + m: make(map[string]Edge, len(g.m)), + } + for id, e := range g.m { + g2.m[id] = e + } + return g2 +} + +// AddEdge 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. +func (g Graph) AddEdge(e Edge) Graph { + id := e.id() + if _, ok := g.m[id]; ok { + return g + } + + g2 := g.cp() + g2.m[id] = e + return g2 +} + +// DelEdge 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. +func (g Graph) DelEdge(e Edge) Graph { + id := e.id() + if _, ok := g.m[id]; !ok { + return g + } + + g2 := g.cp() + delete(g2.m, id) + return g2 +} + +// Values returns all Values which have incoming or outgoing Edges in the Graph. +func (g Graph) Values() []Value { + values := make([]Value, 0, len(g.m)) + found := map[string]bool{} + tryAdd := func(v Value) { + if ok := found[v.ID]; !ok { + values = append(values, v) + found[v.ID] = true + } + } + + for _, e := range g.m { + tryAdd(e.Head) + tryAdd(e.Tail) + } + return values +} + +// Edges returns all Edges which are part of the Graph +func (g Graph) Edges() []Edge { + edges := make([]Edge, 0, len(g.m)) + for _, e := range g.m { + edges = append(edges, e) + } + return edges +} + +// ValueEdges returns all input (e.Head==v) and output (e.Tail==v) Edges +// for the given Value in the Graph. +func (g Graph) ValueEdges(v Value) ([]Edge, []Edge) { + var in, out []Edge + for _, e := range g.m { + if e.Tail.ID == v.ID { + out = append(out, e) + } + if e.Head.ID == v.ID { + in = append(in, e) + } + } + return in, out +} + +// 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 +// performed by passing the cursor value along with its input and output Edges +// into the next function. The cursor moves to the returned Value and next is +// called again, and so on. +// +// If the boolean returned from the next function is false traversal stops and +// this method returns. +// +// If start has no Edges in the Graph, or a Value returned from next doesn't, +// this will still call next, but the in/out params will both be empty. +func (g Graph) Traverse(start Value, next func(v Value, in, out []Edge) (Value, bool)) { + curr := start + var ok bool + for { + in, out := g.ValueEdges(curr) + if curr, ok = next(curr, in, out); !ok { + return + } + } +} diff --git a/graph/graph_test.go b/graph/graph_test.go new file mode 100644 index 0000000..111c871 --- /dev/null +++ b/graph/graph_test.go @@ -0,0 +1,135 @@ +package graph + +import ( + . "testing" + "time" + + "github.com/mediocregopher/mediocre-go-lib/mrand" + "github.com/mediocregopher/mediocre-go-lib/mtest/massert" + "github.com/mediocregopher/mediocre-go-lib/mtest/mchk" +) + +func TestGraph(t *T) { + type state struct { + Graph + + m map[string]Edge + } + + type params struct { + add Edge + del Edge + } + + strV := func(s string) Value { + return Value{ID: s, V: s} + } + + chk := mchk.Checker{ + Init: func() mchk.State { + return state{ + m: map[string]Edge{}, + } + }, + Next: func(ss mchk.State) mchk.Action { + s := ss.(state) + var p params + if i := mrand.Intn(10); i == 0 { + // add edge which is already there + for _, e := range s.m { + p.add = e + break + } + } else if i == 1 { + // delete edge which isn't there + p.del = Edge{Tail: strV("z"), Val: strV("z"), Head: strV("z")} + } else if i <= 5 { + // add probably new edge + p.add = Edge{ + Tail: strV(mrand.Hex(1)), + Val: strV(mrand.Hex(1)), + Head: strV(mrand.Hex(1)), + } + } else { + // probably del edge + p.del = Edge{ + Tail: strV(mrand.Hex(1)), + Val: strV(mrand.Hex(1)), + Head: strV(mrand.Hex(1)), + } + } + return mchk.Action{Params: p} + }, + Apply: func(ss mchk.State, a mchk.Action) (mchk.State, error) { + s, p := ss.(state), a.Params.(params) + if p.add != (Edge{}) { + s.Graph = s.Graph.AddEdge(p.add) + s.m[p.add.id()] = p.add + } else { + s.Graph = s.Graph.DelEdge(p.del) + delete(s.m, p.del.id()) + } + + { // test Values and Edges methods + vals := s.Graph.Values() + edges := s.Graph.Edges() + var aa []massert.Assertion + found := map[string]bool{} + tryAssert := func(v Value) { + if ok := found[v.ID]; !ok { + found[v.ID] = true + aa = append(aa, massert.Has(vals, v)) + } + } + for _, e := range s.m { + aa = append(aa, massert.Has(edges, e)) + tryAssert(e.Head) + tryAssert(e.Tail) + } + aa = append(aa, massert.Len(vals, len(found))) + aa = append(aa, massert.Len(edges, len(s.m))) + if err := massert.All(aa...).Assert(); err != nil { + return nil, err + } + } + + { // test ValueEdges + for _, val := range s.Graph.Values() { + in, out := s.Graph.ValueEdges(val) + var expIn, expOut []Edge + for _, e := range s.m { + if e.Tail.ID == val.ID { + expOut = append(expOut, e) + } + if e.Head.ID == val.ID { + expIn = append(expIn, e) + } + } + if err := massert.Comment(massert.All( + massert.Subset(expIn, in), + massert.Len(in, len(expIn)), + massert.Subset(expOut, out), + massert.Len(out, len(expOut)), + ), "val:%q", val.V).Assert(); err != nil { + return nil, err + } + } + } + + return s, nil + }, + MaxLength: 10, + } + + err := chk.RunCase( + params{add: Edge{Tail: strV("4"), Val: strV("d"), Head: strV("4")}}, + params{del: Edge{Tail: strV("4"), Val: strV("d"), Head: strV("4")}}, + ) + if err != nil { + t.Fatal(err) + } + + if err := chk.RunFor(5 * time.Second); err != nil { + t.Fatal(err) + } +}