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