Skip to content

Instantly share code, notes, and snippets.

@jordanorelli
Created November 27, 2021 00:04
Show Gist options
  • Save jordanorelli/491af5974f095e23f547f2704956746f to your computer and use it in GitHub Desktop.
Save jordanorelli/491af5974f095e23f547f2704956746f to your computer and use it in GitHub Desktop.
an abomination
package main
import (
"fmt"
"strings"
)
type A int
func (a *A) Merge(v *A) error {
*a += *v
return nil
}
func (a A) String() string { return fmt.Sprint(int(a)) }
func newA(v int) *A {
a := A(v)
return &a
}
type B int
func (b *B) Merge(v *B) error {
*b *= *v
return nil
}
func (b B) String() string { return fmt.Sprint(int(b)) }
func newB(v int) *B {
b := B(v)
return &b
}
// Bad is an example of something bad that I do -not- want to select for, it
// has a Merge method but the parameter is not the same as the receiver.
type Bad int
func (b *Bad) Merge(v *A) error {
*b /= Bad(*v)
return nil
}
func newBad(v int) *Bad {
b := Bad(v)
return &b
}
type merges[X any] interface {
Merge(X) error
}
// Merge takes two values of any type that can merge with itself.
func Merge[X merges[X]](x X, y X) error {
return x.Merge(y)
}
type Slice[X merges[X]] []X
func (s Slice[X]) All() X {
var x X
for _, v := range s {
x.Merge(v)
}
return x
}
type Pair[X merges[X], Y merges[Y]] struct {
Left X
Right Y
}
func cons[X merges[X], Y merges[Y]](x X, y Y) Pair[X, Y] {
return Pair[X, Y]{Left: x, Right: y}
}
// Table is a maping of keys to values where the keys are anything comparable
// and the values are any type that define their own semantics for merging one
// value into another value
type Table[K comparable, X merges[X]] map[K]X
// Merge lets you merge two tables together, performing a member-wise merge for
// keys that exist in both tables, and accepting the values from the provided
// table if no value existed in the receiver
func (t Table[K, X]) Merge(from Table[K, X]) error {
for k, m := range from {
existing, ok := t[k]
if !ok {
t[k] = m
continue
}
if err := existing.Merge(m); err != nil {
return fmt.Errorf("merge error at key %s: %w", k, err)
}
}
return nil
}
// ruin takes a function that has a parameter and returns a new function that
// removes the type from that parameter. This is really unbelievably gross.
func ruin[X any](f func(X) error) func (interface{}) error {
return func(v interface{}) error {
vv, ok := v.(X)
if !ok {
return fmt.Errorf("unexpected type: %T wanted: %T", v, vv)
}
return f(vv)
}
}
// erase takes any mergeable type and separates the value from the merge
// function.
func erase[X merges[X]] (x X) erased {
return erased{
val: x,
mergeFn: ruin(x.Merge), // <-- where the magic happens!
}
}
// erased is a mergeable value that can merge with other erased values at
// runtime
type erased struct {
val interface{}
mergeFn func(interface{}) error
}
func (e erased) Merge(v erased) error { return e.mergeFn(v.val) }
// Namespace is a key-value mapping of strings to values that can merge against
// other values of their own type.
type Namespace struct {
// members is unexported. Members cannot be accessed directly.
members Table[string, erased]
}
func (n Namespace) String() string {
var s strings.Builder
s.WriteRune('{')
first := true
for k, m := range n.members {
if !first {
s.WriteString(", ")
}
first = false
fmt.Fprintf(&s, "%q: %v (%T)", k, m.val, m.val)
}
s.WriteRune('}')
return s.String()
}
func NewNamespace() Namespace {
return Namespace{members: make(map[string]erased)}
}
// Add is the only way to add values to a Namespace. It has to be implemented
// as a package-level function because methods can't accept type parameters.
func Add[X merges[X]] (n Namespace, key string, x X) { n.members[key] = erase(x) }
func (n Namespace) Merge(from Namespace) error {
// Merging a namespace just means merging its members table with the
// members table of another namespace.
return n.members.Merge(from.members)
}
func main() {
a1 := A(3)
a2 := A(5)
b1 := B(7)
b2 := B(3)
// Merge takes two values of any type that define how to merge values of
// that type and merges them.
fmt.Println(Merge(&a1, &a2))
fmt.Println(a1)
fmt.Println(Merge(&b1, &b2))
fmt.Println(b1)
// It's a compile-time error to try to merge two values of differing types
// fmt.Println(Merge(&a1, &b1))
// It's also a compile-time error to try to merge two values of the same
// type if that type does not define how it can be merged
// fmt.Println(Merge("harry", "sally"))
// Bad doesn't actually satisfy the merge constraint because it doesn't
// merge with its own type, it merges with a type other than itself. This
// is a compile-time error:
//
// fmt.Println(Merge(newBad(10), newBad(13)))
// these all have the same type
alice := Table[string, *A]{
"chocolate": newA(1),
"vanilla": newA(1),
"strawberry": newA(3),
"pistacchio": newA(5),
}
bob := Table[string, *A]{
"chocolate": newA(2),
"vanilla": newA(3),
"banana": newA(1),
"hazlenut": newA(4),
}
fmt.Printf("alice: %v\n", alice)
fmt.Printf("bob: %v\n", bob)
fmt.Printf("merge error when merging alice and bob: %v\n", Merge(alice, bob))
fmt.Printf("alice after merging with bob: %v\n", alice)
// while a Table requires that every value be of a single type, a Namespace
// allows you to store a value of any type so long as the type of that
// value defines how it will merge with other values of its own type
n := NewNamespace()
// the keys "chocolate" and "vanilla" associate to different types
Add(n, "chocolate", newB(1))
Add(n, "vanilla", newA(1))
Add(n, "strawberry", newA(3))
Add(n, "pistacchio", newA(5))
fmt.Printf("n: %v\n", n)
// These two Namespaces have a few pairs where the key name and the type
// are the same, so the values can be merged.
n2 := NewNamespace()
// since "chocolate" and "vanilla" have the same type in both n and n2, the
// namespaces n and n2 can still do a member-wise merge
Add(n2, "chocolate", newB(2))
Add(n2, "vanilla", newA(3))
Add(n2, "banana", newA(1))
Add(n2, "hazlenut", newA(4))
fmt.Printf("n2: %v\n", n2)
fmt.Println(Merge(n, n2))
fmt.Printf("n merged with n2: %v\n", n)
n3 := NewNamespace()
Add(n3, "chocolate", newB(2))
// vanilla has a different type in namespace n3 than it does in the
// namespaces n and n2, which will cause n3 fail to merge with the other
// two namespaces at runtime.
Add(n3, "vanilla", newB(3))
Add(n3, "banana", newA(1))
Add(n3, "hazlenut", newA(4))
fmt.Printf("n3: %v\n", n2)
// here we get a runtime error, you can't merge these namespaces because
// there's a key that exists in both but has a mis-matched value type
fmt.Println(Merge(n, n3))
// These are compile time errors:
// Add(n3, "lobster", newBad(5))
// Add(n3, "mac-and-cheese", "zombo.com")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment