i am doing crimes. i am also thinking out loud.
look, before you start, i am well aware that go has interfaces, and not union types
the problem with interfaces is simple: go does not know how to deserialize json into a "field MyInterface" type, at least not for json. the json encoder does have a few useful interfaces built in, but it isn't solving the problem at hand.
let's take an example
i have two objects "BigTask" and "SmallTask" which both implement "TaskInterface", and I have some containing object which has a "task TaskInterface" field. i turn it into json, everything's fine. i try and read it back, all hell breaks loose.
and yes, i know the standard solution. instead of using an interface, i should use a "wide struct", with one field per possible type. for the above example, it might look like this: type Task struct {big BigTask; small SmallTask}. it does force the json to look a specific way, an object of {"big":...} or {"small":...}
this works great if you have two, maybe three alternatives, but it gets a little messy with twenty. terrible with a hundred. that's why you end up doing tagged interfaces.
type VariantTask struct {
kind string
value TaskInterface
}
then you write custom json decoding methods which work out if it's a Big or a Small task that needs creating. it's a bit of a hack, but now your enclosing structs have VariantTask fields, and you've written Big() and Small() methods. you can even make VariantTask obey TaskInterface, and forward on all the methods too.
There's a design choice here, too. How many methods do we put on the interface?
A wide interface makes it easy to implement the variant type. It just forwards every method to the underlying object. A wide interface means that every underlying object has to implement all the methods, and that number adds up pretty quickly.
On the other hand, you could go for having one method on the interface, something like response = obj.Invoke(Action), but now your variant struct methods have to do a little bit more work turning things into actions, and then unpacking responses.
Either way, when you use an interface, every inner object has to implement it. With a Big and Small task, that's not really much of an issue. If your inner object is is a thing like "a string", you end up having to write a StringContainer struct which implements your VariantInterface.
In other words: using an interface to forward methods isn't that great. A wide interface involves writing a ton of methods, a narrow one involves writing less methods but a bit more boilerplate, and you have to wrap basic objects to fufill it, too.
So what if we didn't use an interface.
What if we used vtables.
type Variant struct {
descriptor *Descriptor
value any
}
type Descriptor struct {
name string
method func(any, string) bool
}
func (v Variant) Method(key string) bool {
return v.descriptor.method(v.value, out)
}
Hell, we could just go with
type Variant struct {
descriptor func(Request) Response
value any
}
I did not expect to be seriously considering "vtables in go", but:
-
The value is
any. I can put an int, or whatever in there, no worries. I don't have to make any wrapper types to store a value inside the Variant -
The functions in the descriptor struct can be optional. I don't have to write any extra boilerplate to return error values.
