Idiomatic Code Reuse in Go
There was a lament (not the first) in #go-nuts today that go's interfaces cannot accept any implementation. It wasn't the first such complaint, and this isn't the first exploration of why it might not be a great idea.
Regardless, some interesting golang-ish code was provided which demonstrated the concept and showed how a re-imagining of golang interfaces as traits could allow code reuse between different structs by simply allowing interfaces to implement functions themselves:
type Moveable trait {
Pos() int
SetPos(int)
Move(m Movable, v int) {
m.SetPos(m.Pos() + v)
}
}
type Entity struct {
pos int
}
func (e *Entity) Pos() int {
return e.pos
}
func (self *Entity) SetPos(v int) {
e.pos = v
}
e := new(Entity)
e.Move(4)
There are a few things to note here. A decision has to be made if Entity implements its own Move() function. The calling conventions for the shared code looks very natural and still allows for mutability via other interfaces fulfilled by the implementer. Worryingly, embedding the trait would work quite differently from embedding a struct or an interface: embedding an interface does nothing, and when embedding structs, the outer struct inherits the implementation of the inner, but the receiver of calls on that interface is the inner struct (which has its own attributes), not the outer.
Here is how I've achieved this type of code sharing in go up to this point:
type Moveable interface {
Pos() int
SetPos(int)
}
func Move(m Moveable, v int) {
m.SetPos(m.Pos() + v)
}
type Entity struct {
pos int
}
func (e *Entity) Pos() int {
return e.pos
}
func (e *Entity) SetPos(v int) {
e.pos = v
}
e := new(Entity)
Move(e, 4)
Aside from having the virtue of currently working, this allows us to keep the very simple and straightforward interface, struct, and method concepts, dodge the difficult conceptual/philosophical questions above, and still duck type Move to work on anything implementing Movable. The resultant library interface is a bit less attractive and, for some things, it could make certain types of method chaining difficult or impossible. In practice, if this were a library, the package name can provide some of that missing context.
In the end, go is a procedural language, and while it has some object oriented concepts like encapsulation and interface contracts, it's a bad idea to try to bend that into something it's not:
- If you have shared behavior, that's an interface
- If you have shared implementation, that's a function