3 ms·
This is just a case of non-idiomatic programming in the first place, and it's not even non-idiomatic "Go" programming, it's non-idiomatic for object-oriented la
by jerf 5y ago
This is just a case of non-idiomatic programming in the first place, and it's not even non-idiomatic "Go" programming, it's non-idiomatic for object-oriented languages in general. The correct solution to this sort of problem is more like this:
type OptionParser interface {
ParseValue(data string) (*Value, error)
}
type OptionField struct {}
func (of *OptionField) ParseValue(data string) (*Value, error) {
return &Value{OptionID: data}, nil
}
type NumericField struct {}
func (nf *NumericField) ParseValue(data string) (*Value, error) {
value, err := strconv.ParseFloat(data, 64)
if err != nil {
return nil, errors.New("invalid value, must be a number")
}
return &Value{Numeric: value}, nil
}
and so on. A non-Go language would use whatever the local Abstract Data Type abstraction is. The parseValue function just melts away entirely, you just call a method on the value you have in hand. And now, instead of having already spent a great deal of design budget on this abstraction, and having a hard time integrating it into any future abstractions you may want to make, you're using language constructs in the way they were designed, and there's more space here to make further adaptations. Generics could even help with some of those perhaps, but they're not needed here.
It's possible the Value type could be helped a bit with generics, but it's not clear. Probably the old-school existing "sum types" techniques would work just fine: http://www.jerf.org/iri/post/2917 http://www.jerf.org/iri/post/2917 But "a field per possible value" isn't necessarily a bad choice, especially if it's only a handful of fields. Sometimes it's the best performing implementation, even if it may seem skeevy at the programmer level, because it involves the least indirection even if it seems to involve the largest (in RAM size) objects, and being able to pass them around by value rather than pointers or interfaces means inlining may end up doing more for you too. In general, nowadays, you're better off using 64 bytes in one place than you are using 16 bytes here and 16 bytes somewhere else through a pointer, even if 64 > 32.
It is helpful to understand the expression problem to understand what's going on here: https://en.wikipedia.org/wiki/Expression_problem https://en.wikipedia.org/wiki/Expression_problem In short, OO excels in adding values that implement an existing set of operations, functional programming excels in adding operations to existing sets of data types. Forcing sum types here into a language that doesn't really want them means that you're taking a techique that is good at the wrong dimension (you are adding more data types here that fit into the existing operations, what OO is already good at), then having to twist it around in a way it isn't really comfortable with to get it to work with what you want (in this case it'll manifest as needing to add this code to every method, of which I'm sure you have more than the ones shown; in properly-applied FP you get usually get the advantage by adding many fewer clauses to your pattern matches, often just one), when the correct solution was already there.
While I often complain about trying to jam FP into OO without understanding in the context of map/filter/reduce-based programming, this is actually the even more profound difference between the two. It's nice to have the FP toolkit around in an OO language for when you encounter a problem it can help with, but when you're in an OO language and right in OO's wheelhouse, use the OO. Don't jam the FP in because FP is Platonically better or something. It actually has well-characterizable weaknesses vs. object orientation, and at least as it stands now is absolutely not universally better at everything.
(Some up-and-coming languages claim to have solved the expression problem. I have not evaluated any of these claims. More power to them if they are correct!)
- lawrjone 5y agoHey, thanks for the response! I disagree with you that this is non-idiomatic Go code, but that's by-the-by: no one can agree on what the idiomatic way really is, anyways. On your solution: you're right in that you can define separate types for each of these values, then have them implement a common interface. But that ends up getting pretty weird for our codebase, as we pass the types around between a variety of packages and re-typing them so you can implement the handler code in that specific place leads to some really awkward code organisation, and I think would be a net worse. I think this is your article? http://www.jerf.org/iri/post/2917 http://www.jerf.org/iri/post/2917 If so, you say yourself: > You also have no compiler support for detecting that you missed a case. Again, Go's m.o., so shrug. The original article is just about getting us compiler support for catching the missing cases, in a way that we can easily change existing switch statements to support it. It shouldn't be offensive, and it definitely didn't take any of our 'design' budget: it's very simple, and took less than an hour to put together and adopt it in the codebase. Sounds like it's not your cup of tea, but it is a nice improvement for us :)
- jerf 5y agoI was talking about a Value type being a sum type. Your widget type shouldn't be a sum type at all. For your new widget, you define an interface and you call a method. You do get full compiler support for ensuring that the method exists because if you try to pass the new widget type in to something that defines the interface, and it does not implement the interface, it is a compile-time error. This is absolutely unidiomatic. You pay for implementing a foreign paradigm into Go code, then you pay for that foreign paradigm not doing the task you're asking it to do very well, where the native paradigm does it smoothly and correctly. This is doubly-non-idiomatic code; it's non-idiomatic Go and it's non-idiomatic FP!
- woah 5y agoSorry, but interfaces are not a replacement for sum types. Trying to shoehorn every situation where you might want to deal with multiple different types of things into figuring out what common interface they could implement is a pretty terrible pattern (although interfaces are great where they are actually applicable). The Go solution of using reflection on empty interfaces and type switches when the interface pattern fails instead of sum types is a hack that forces you to throw away type safety. Go <1.18 often feels like it is an half-finished language, except there are a lot of people ready to jump down your throat about how it's actually good that it is half-finished. Real "ignorance is strength" vibes. And I'm someone who writes Go every day.