some merge stuff

master
Jordan Orelli 3 years ago
parent 141f8d3e67
commit bac72ab459

@ -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)
}
}

@ -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)
}
})
}

@ -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)
}

@ -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")
}
})
}

@ -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
}

@ -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)
}
Loading…
Cancel
Save