some merge stuff
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…
Reference in New Issue