From bd41cba861d3ddc4aaeca72fc39404b3fe5d05ff Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sun, 5 Dec 2021 20:40:30 -0600 Subject: [PATCH] switchin puters this stuff doesn't work --- crdt/doc.go | 2 + crdt/gcounter.go | 9 +++ crdt/pncounter.go | 6 ++ merge2/merge.go | 157 +++++++++++++++++++++++++++++++++++-------- merge2/merge_test.go | 42 ++++++++---- ref/ref.go | 2 +- 6 files changed, 178 insertions(+), 40 deletions(-) create mode 100644 crdt/doc.go create mode 100644 crdt/pncounter.go diff --git a/crdt/doc.go b/crdt/doc.go new file mode 100644 index 0000000..bfc1862 --- /dev/null +++ b/crdt/doc.go @@ -0,0 +1,2 @@ +// crdt provides conflict-free replicated data types +package crdt diff --git a/crdt/gcounter.go b/crdt/gcounter.go index f91fa62..da8dc11 100644 --- a/crdt/gcounter.go +++ b/crdt/gcounter.go @@ -16,10 +16,19 @@ func NewGCounter[K comparable]() GCounter[K] { return GCounter[K]{slots: make(map[K]int)} } +// GCounter is a grow-only counter. +// +// In the general case, some N hosts will read and write to the gcounter and +// periodically merge their state, providing eventual consistency and allowing +// all nodes to write. Each node must have a unique ID, and should write into +// the slot that associates to that ID. The slot ID is not embedded into the +// gcounter itself, and the assignment of IDs to nodes is not provided. type GCounter[K comparable] struct { slots map[K]int `json:"slots"` } +// Incr increments the value in the gcounter at the provided slot. Callers must +// provide the slot to be incremeneted. func (g GCounter[K]) Incr(slot K) error { var zero K if slot == zero { diff --git a/crdt/pncounter.go b/crdt/pncounter.go new file mode 100644 index 0000000..0f761c5 --- /dev/null +++ b/crdt/pncounter.go @@ -0,0 +1,6 @@ +package crdt + +// PNCounter is a positive-negative counter +type PNCounter[K comparable] struct { + slots map[K][2]int +} diff --git a/merge2/merge.go b/merge2/merge.go index dbd6e15..5b606b6 100644 --- a/merge2/merge.go +++ b/merge2/merge.go @@ -2,9 +2,16 @@ package merge2 import ( "fmt" + "errors" ) +var typeMismatch = errors.New("type mismatch") + +// Merges[X] is any type that can merge itself into some *X type Merges[X any] interface { + // MergeInto should merge the receiver into the parameter *X. Callers are + // expected to mutate the parameter in some way, returning any error + // encountered when attempting to do so. MergeInto(*X) error } @@ -17,9 +24,11 @@ type Merges[X any] interface { // } // return dest, nil // } +// // Into merges any number of values into some commonly-targetable value. -func Into[X Merges[Y], Y any](dest *Y, sources ...X) error { +// func Into[X any](dest *X, sources ...Merges[X]) error { +func Into[X any, Y Merges[X]](dest *X, sources ...Y) error { for _, src := range sources { if err := src.MergeInto(dest); err != nil { return err @@ -28,6 +37,9 @@ func Into[X Merges[Y], Y any](dest *Y, sources ...X) error { return nil } +// Maps merges two maps. All keys that appear in source are merged into dest. +// Keys that appear in src but not in dest merge into the zero value of T and +// then store in dest. func Maps[K comparable, V Merges[T], T any](dest map[K]T, src map[K]V) error { for k, v := range src { dv := dest[k] @@ -59,49 +71,140 @@ func (m Map[K, V]) MergeInto(dest map[K]V) error { return nil } +// Emerge is an empty merge: given some merge source, emerge merges the merge +// source into the zero value of the merge destination type. +// +// Emerge[Y any](x Merges[Y]) (Y, error) +func Emerge[X Merges[Y], Y any](x X) (Y, error) { + var y Y + return y, x.MergeInto(&y) +} + +// Boxed is a type-erased container for merge semantics. +// +// Boxed exists in order to facilitate the merging of heterogeneous collections +// of values. type Boxed struct { - p interface{} - merge func(interface{}) error + // the value inside of the box. Since the only way to put a value in a box + // is through the Box function or by merging from another box, we know that + // this value defines some merge function + val interface{} + + // mkdest creates a value of the type to which the val field merges + mkdest func() interface{} + + // homologous describes whether or not the contained value merges with its + // own type. Any boxed type that defines merge semantics against its own + // type can merge into an empty box. + homologous bool + + // mergeInto is a function that defines how to merge the val field into + // some destination + mergeInto func(dest interface{}) error } func (b Boxed) String() string { - return fmt.Sprint(b.p) + return fmt.Sprint(b.val) } +// IsEmpty is true for boxes that contain nothing func (b Boxed) IsEmpty() bool { - return b.p == nil + return b.val == nil +} + +// IsTerminal describes whether or not the box defines any merge semantics. A +// box containing a value that does not define merge semantics is a box that +// terminates a merge chain. +func (b Boxed) IsTerminal() bool { + return b.mergeInto == nil +} + +// erasef1e erases type information from a function of input arity 1 that +// returns an error +func erasef1e[X any](f func(X) error) func(interface{}) error { + return func(v interface{}) error { + tv, ok := v.(X) + if !ok { + return fmt.Errorf("unexpected %T value, expected %T instead: %w", v, tv, typeMismatch) + } + return f(tv) + } +} + +func mergeFn[X Merges[Y], Y any](x X) func(interface{}) error { + return nil + // return func(v interface{}) error { + // x.MergeInto + // } } -func Box[X Merges[X]](x X) Boxed { +// nunu[X] creates a function for the type X that creates a new zero value +// having the type X, then erases the type information by sticking it in an +// empty interface. This is a constructor-constructor that creates a +// type-erased constructor. Pretty gross! +func nunu[X any]() func() interface {} { + return func() interface{} { + var zero X + return zero + } +} + +// sametype determines whether or not some manually instantiated type +// parameters are or are not the same type. +func sametype[X, Y any]() bool { + _, ok := interface{}(*new(X)).(Y) + return ok +} + +// Box boxes a value. Only values that define -some- merge semantic can go into +// the box. +// +// Box[Y any](x Merges[Y]) Boxed +func Box[X Merges[Y], Y any](x X) Boxed { return Boxed{ - p: &x, - merge: func(dest interface{}) error { - return fmt.Errorf("not yet") - // dp, ok := dest.p.(*X) - // if !ok { - // return fmt.Errorf("boxed val tried to merge into %T but can only merge into %T", dest, dp) - // } - // if dp == nil { - // return fmt.Errorf("that shit is empty fuck you") - // } - // fmt.Printf("merging %v into %v\n", x, dp) - // return nil - }, + val: x, + mkdest: nunu[Y](), + homologous: sametype[X, Y](), + mergeInto: erasef1e(x.MergeInto), } } +// EndBox creates a terminal box in a merge chain. This boxed value contains a +// value that does not define any merge semantics. It may only be used as a +// merge destination, not a merge source. +func EndBox[X any](x X) Boxed { + return Boxed{ + val: x, + } +} + +// MergeInto merges the Boxed value into some destination value. If the boxed +// value does not merge into the value supplied, the failure occurs at runtime. +// For merge semantics that are type-checked at compile time... don't box your +// values, I dunno what to tell you. func (b Boxed) MergeInto(dest interface{}) error { if b.IsEmpty() { return nil } - return b.merge(dest) -} + if db, ok := dest.(Boxed); ok { + if db.IsEmpty() { + v := b.mkdest() + if b.homologous { + db.homologous = true -func Unbox[X Merges[Y], Y any](b Boxed) (X, error) { - p, ok := b.p.(*X) - if !ok { - var zero X - return zero, fmt.Errorf("type error") + db.mergeInto = erasef1e(v.MergeInto) + db.mkdest = b.mkdest + } + } } - return *p, nil + return b.mergeInto(dest) } + +// func Unbox[X Merges[Y], Y any](b Boxed) (X, error) { +// p, ok := b.val.(*X) +// if !ok { +// var zero X +// return zero, fmt.Errorf("type error") +// } +// return *p, nil +// } diff --git a/merge2/merge_test.go b/merge2/merge_test.go index 1377cf3..31f3e84 100644 --- a/merge2/merge_test.go +++ b/merge2/merge_test.go @@ -109,30 +109,48 @@ func TestMap(t *testing.T) { } func TestBox(t *testing.T) { - t.Run("empty", func (t *testing.T) { + t.Run("both empty", func (t *testing.T) { var a Boxed var b Boxed - if err := a.MergeInto(&b); err != nil { + if err := a.MergeInto(b); err != nil { t.Fatalf("empty boxes failed to merge: %v", err) } }) - // an empty box can merge into anything and it should be a no-op - t.Run("empty source", func (t *testing.T) { + t.Run("empty source", func(t *testing.T) { var a Boxed - b := additive(3) - if err := a.MergeInto(&b); err != nil { - t.Fatalf("empty boxes failed to merge: %v", err) - } - }) + b := Box[additive, additive](additive(3)) - t.Run("homologous", func(t *testing.T) { - a, b := Box(additive(3)), Box(additive(8)) if err := a.MergeInto(b); err != nil { - t.Fatalf("homologous boxes failed to merge: %v", err) + t.Error(err.Error()) } }) + // t.Run("empty destination", func(t *testing.T) { + // a := Box[additive, additive](additive(3)) + // var b Boxed + + // if err := a.MergeInto(b); err != nil { + // t.Error(err.Error()) + // } + // }) + + // an empty box can merge into anything and it should be a no-op + // t.Run("empty source", func (t *testing.T) { + // var a Boxed + // b := additive(3) + // if err := a.MergeInto(b); err != nil { + // t.Fatalf("empty boxes failed to merge: %v", err) + // } + // }) + + // t.Run("homologous", func(t *testing.T) { + // a, b := Box(additive(3)), Box(additive(8)) + // if err := a.MergeInto(b); err != nil { + // t.Fatalf("homologous boxes failed to merge: %v", err) + // } + // }) + // a, b := Box(additive(3)), Box(additive(5)) // if err := b.MergeInto(&a); err != nil { // t.Errorf("merge boxed failed: %v", err) diff --git a/ref/ref.go b/ref/ref.go index 9aa11af..c26f965 100644 --- a/ref/ref.go +++ b/ref/ref.go @@ -19,4 +19,4 @@ type Ref[T any] struct { ptr *T } // Val reads the value for this reference func (r Ref[T]) Val() T { return *r.ptr } -func (r Ref[T]) String() string { return fmt.Sprintf("ref{%s}", *r.ptr) } +func (r Ref[T]) String() string { return fmt.Sprintf("ref{%v}", *r.ptr) }