From bac72ab4598a6902f298f57b10289893fa51aac0 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Sat, 27 Nov 2021 14:33:41 -0600 Subject: [PATCH] some merge stuff --- merge/box.go | 53 ++++++++++++++++++++++++++++++ merge/box_test.go | 68 ++++++++++++++++++++++++++++++++++++++ merge/merge.go | 29 +++++++++++++++++ merge/merge_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++ merge/table.go | 22 +++++++++++++ merge/table_test.go | 38 ++++++++++++++++++++++ 6 files changed, 289 insertions(+) create mode 100644 merge/box.go create mode 100644 merge/box_test.go create mode 100644 merge/merge.go create mode 100644 merge/merge_test.go create mode 100644 merge/table.go create mode 100644 merge/table_test.go diff --git a/merge/box.go b/merge/box.go new file mode 100644 index 0000000..2fa44e1 --- /dev/null +++ b/merge/box.go @@ -0,0 +1,53 @@ +package merge + +import ( + "fmt" + "errors" +) + +// Boxed is a boxed value that is able to merge with other values. Boxing a +// value pushes its type-checking from compile time to run time. This can be +// useful in the event that you wish to construct mergeable values with +// heterogeneous members, but should likely be avoided otherwise. +type Boxed struct { + val interface{} + merge func(interface{}) error +} + +func (b Boxed) Merge(from Boxed) error { + if err := b.merge(from.val); err != nil { + return fmt.Errorf("boxed merge failed: %w", err) + } + return nil +} + +var typeMismatch = errors.New("mismatched types") + +func strip[X any](f func(X) error) func(interface{}) error { + return func(v interface{}) error { + vv, ok := v.(X) + if !ok { + return fmt.Errorf("unable to merge value of type %T into value of type %T: %w", v, vv, typeMismatch) + } + return f(vv) + } +} + +// Box takes a mergeable value and creates a new mergeable value of type Boxed. +// Any two Boxed values can attempt to merge at runtime. +func Box[X Merges[X]](x X) Boxed { + return Boxed{ + val: x, + merge: strip(x.Merge), + } +} + +// Unbox removes a value from its box. +func Unbox[X Merges[X]](b Boxed) (X, error) { + if v, ok := b.val.(X); ok { + return v, nil + } else { + var zero X + return zero, fmt.Errorf("box contains %T, not %T: %w", b.val, zero, typeMismatch) + } +} diff --git a/merge/box_test.go b/merge/box_test.go new file mode 100644 index 0000000..66ee5df --- /dev/null +++ b/merge/box_test.go @@ -0,0 +1,68 @@ +package merge + +import ( + "testing" + "errors" +) + +func TestBox(t *testing.T) { + t.Run("matching", func(t *testing.T) { + a, b := add(3), add(7) + dest, src := Box(a), Box(b) + if err := Merge(dest, src); err != nil { + t.Fatalf("unexpected error merging boxes: %v", err) + } + + if a.total != 10 { + t.Error("box failed to mutate contents in merge") + } + if b.total != 7 { + t.Error("box mutated source contents in merge for some reason") + } + }) + + t.Run("mismatched", func(t *testing.T) { + a, b := add(3), mul(7) + dest, src := Box(a), Box(b) + err := Merge(dest, src) + if err == nil { + t.Fatalf("mismatched merge succeeded but should have failed") + } + if !errors.Is(err, typeMismatch) { + t.Fatalf("merge gave unexpected error value: %v", err) + } + + + if a.total != 3 { + t.Error("failed merge still mutated values") + } + }) +} + +func TestUnbox(t *testing.T) { + t.Run("matching", func(t *testing.T) { + a := add(3) + b := Box(a) + + v, err := Unbox[*additive](b) + if err != nil { + t.Fatalf("unexpected unbox error: %v", err) + } + if v.total != 3 { + t.Fatalf("boxing and unboxing messed up the value somehow") + } + }) + + t.Run("mismatched", func(t *testing.T) { + a := add(3) + b := Box(a) + + v, err := Unbox[*multiplicative](b) + if err == nil { + t.Fatalf("mismatched unboxing should have returned an error but succeeded and unboxed %v instead", v) + } + if !errors.Is(err, typeMismatch) { + t.Fatalf("unbox expected to give typeMismach but instead gave unexpected error: %v", err) + } + }) +} diff --git a/merge/merge.go b/merge/merge.go new file mode 100644 index 0000000..08fdf48 --- /dev/null +++ b/merge/merge.go @@ -0,0 +1,29 @@ +package merge + +// Merges defines the ability to merge a value with another value. Note that +// while any type can satisfy Merges[X], practically speaking this interface is +// only useful to form the constraint [X Merges[X]]. E.g., in the following +// example, the type A merges with itself: +// +// type A struct {} +// func (a *A) Merge(*A) error { ... } +// +// This is the recommended pattern of implementation for utilizing the Merges +// interface. In this example, the type B merges with a different, type, type C: +// +// type B struct {} +// type C struct {} +// func (b *B) Merge(*C) error { ... } +// +// Although the type B satisfies the interface Merges[*C], it does not satisfy +// the constraint [X Merges[X]], which is what is used throughout this package. +type Merges[X any] interface { + Merge(X) error +} + +// Merge takes two values of any type that define their own semantics of how to +// merge one value into another value of the same type. X in this case must be +// a mutable type. +func Merge[X Merges[X]](dest, src X) error { + return dest.Merge(src) +} diff --git a/merge/merge_test.go b/merge/merge_test.go new file mode 100644 index 0000000..fe5d2d9 --- /dev/null +++ b/merge/merge_test.go @@ -0,0 +1,79 @@ +package merge + +import ( + "testing" +) + +// additive implements an additive merge. Merging the two numbers together adds +// the source value into the receiver without changing the source value. +type additive struct { + total int +} + +func (a *additive) Merge(b *additive) error { + a.total += b.total + return nil +} + +func add(n int) *additive { + return &additive{total: n} +} + +type multiplicative struct { + scale int +} + +func (m *multiplicative) Merge(v *multiplicative) error { + m.scale *= v.scale + return nil +} + +func mul(n int) *multiplicative { + return &multiplicative{scale: n} +} + +// exclusive implements an exlsive merge. Merging the two numbers together adds +// the value from the source into the destination, removing it from the source. +type exclusive struct { + stock int +} + +func (e *exclusive) Merge(source *exclusive) error { + e.stock += source.stock + source.stock = 0 + return nil +} + +func ex(n int) *exclusive { + return &exclusive{stock: n} +} + +func TestMerge(t *testing.T) { + t.Run("additive", func(t *testing.T) { + a, b := add(4), add(7) + if err := Merge(a, b); err != nil { + t.Errorf("merge error: %v", err) + } + if a.total != 11 { + t.Errorf("merge failed to mutate destination") + } + if b.total != 7 { + t.Errorf("merged caused unexpected mutation") + } + }) + + t.Run("exclusive", func(t *testing.T) { + a, b := ex(4), ex(7) + if err := Merge(a, b); err != nil { + t.Errorf("merge error: %v", err) + } + if a.stock != 11 { + t.Errorf("merge failed to mutate destination") + } + if b.stock != 0 { + t.Errorf("merge failed to mutate source") + } + }) +} + + diff --git a/merge/table.go b/merge/table.go new file mode 100644 index 0000000..609b553 --- /dev/null +++ b/merge/table.go @@ -0,0 +1,22 @@ +package merge + +import ( + "fmt" +) + +type Table[K comparable, V Merges[V]] map[K]V + +func (t Table[K, V]) Merge(from Table[K, V]) error { + for k, v := range from { + e, ok := t[k] + if !ok { + t[k] = v + continue + } + + if err := e.Merge(v); err != nil { + return fmt.Errorf("tables failed to merge: %w", err) + } + } + return nil +} diff --git a/merge/table_test.go b/merge/table_test.go new file mode 100644 index 0000000..cb1affd --- /dev/null +++ b/merge/table_test.go @@ -0,0 +1,38 @@ +package merge + +import ( + "testing" +) + +func TestMergeTables(t *testing.T) { + alice := Table[string, *additive]{ + "vanilla": add(3), + "chocolate": add(5), + "strawberry": add(2), + } + + bob := Table[string, *additive]{ + "vanilla": add(2), + "chocolate": add(3), + "pistacchio": add(5), + } + + votes := make(Table[string, *additive]) + if err := votes.Merge(alice); err != nil { + t.Fatalf("tables failed to merge: %v", err) + } + if err := votes.Merge(bob); err != nil { + t.Fatalf("tables failed to merge: %v", err) + } + + check := func(k string, n int) { + if have := votes[k].total; have != n { + t.Fatalf("expected %d votes for %s but saw %v instead", n, k, have) + } + } + check("vanilla", 5) + check("chocolate", 8) + check("strawberry", 2) + check("pistacchio", 5) + +}