5 ms·
The problem is that these data structures don't have behaviour. They're pure, immutable data. The fact that I have to wrap them in an interface is also a perfo
by lobster_johnson 10y ago
The problem is that these data structures don't have behaviour. They're pure, immutable data.
The fact that I have to wrap them in an interface is also a performance concern, because an interface devolves to a pointer if it can't fit the contents into a single machine word. So in effect, every single struct becomes a pointer, which in my case seems unnecessary.
There are some concerns that cut across all uses of the data that could be methods, but even things like mapping over (transforming) the data shouldn't really need to be built into the data structures via interfaces. Not least because it's horrible to write. For example, consider this:
type Expression interface{}
type And struct {
LHS, RHS Expression
}
You want to be able to do something like this:
newExpr := Map(expr, func(e Expression) Expression {
if and, ok := e.(And); ok {
if and.LHS == True {
return and.RHS
}
if and.RHS == True {
return and.LHS
}
}
return e
})
How do you implement Map? You have several choices. You can let Expression have Map(), and then every expression needs to implement it:
type Expression interface {
Map(fn func(e Expression) Expression) Expression
}
func (e And) Map(fn func(e Expression) Expression) Expression {
e.LHS = fn(e.LHS)
e.RHS = fn(e.RHS)
return fn(e)
}
Or you could do something like:
type Expression interface {
GetChildren() []Expression
SetChildren(...Expression) Expression
}
func (e And) GetChildren() {
return []interface{e.LHS, e.RHS}
}
func (e And) SetChildren(children ...Expression) Expression {
e.LHS = children[0]
e.RHS = children[1]
return e
}
Now you can implement Map() once for all expressions:
func Map(e Expression, fn func(e Expression) Expression) Expression {
children := e.GetChildren()
newChildren := make([]interface{}, len(children))
for i, child := range children {
newChildren[i] = fn(child)
}
return e.SetChildren(newChildren...)
}
...but at the cost of being expensive, due to slices.
Less expensive idea: Since all my expressions are nonary, unary or binary, I could have a Nonary/Unary/Binary to avoid GetChildren() and SetChildren() operating on slices, and then Map() could simply do a switch.
Or you could simply skip all of that and build a switch statement:
func Map(expr Expression, fn func(e Expression) Expression) Expression {
switch e := expr.(type) {
case And:
e.LHS = fn(e.LHS)
e.RHS = fn(e.RHS)
// ...
}
return fn(expr)
}
This is why I was considering generating code, because it's terrible having to write and quality-check all that boilerplate.
(There's a lot of boilerplate: Map() is not the only function I need. I also need a top-to-bottom version (the above is depth-first) and a Find(), and multiple other things that need to recursively determine something about an expression, without knowing all the types involved.)