6 ms·
>> generics reduce complication That goes against the definition of the word complication. To complicate something is to combine and intertwine it with other c
by CraigJPerry 5y ago
>> generics reduce complication
That goes against the definition of the word complication. To complicate something is to combine and intertwine it with other concerns. To fold them together is to complicate them.
To generify a function is to complicate it with the ability to accept multiple types rather than just one.
There are totally great use cases for generics but all the cases I’ve seen are in library code not in general application programming which is where most developers spend most of our time. But of course the shiny toy can be hard to resist and so generics are often overused and abused.
- stouset 5y agoAs a general rule, if you're referencing the dictionary definition of a word to make your point, you're just playing semantic games. You know what people also find complicated? Hundreds of lines code being repeated with superficial edits because of golang's lack of ability to abstract higher-level ideas. It's a stupid toy example, but for a very large number of people nums.take(20).select(&:odd).reduce(&:+) is less complicated than sum := 0 for i, v := range nums { if i > 20 { break; } if v % 2 == 1 { continue; } sum += v; } The former is less complicated for those people (including me) because it expresses high-level intent rather than low-level implementation. Multiplied over a large code-base, the ability to express intent adds up to an enormous saving in mental overhead rather quickly. You can always drill down and focus on implementation where it matters, but with golang there's almost zero ability to separate a program's detailed implementation from a high-level description of what you're actually trying to accomplish. The Ruby version of this is also less bug-prone (did you spot the bug in the go example?). And it's also easier to apply to new contexts: the former works automatically for infinite enumerations.
- cdelsolar 5y agoshould be == 0? I don't like the Ruby example myself.
- stouset 5y agoShould be >= 20. Whether or not you like the Ruby version is beside my point, which is that which one of those is "more complicated" is a matter of perspective. The Ruby one is almost strictly a wrapper around the golang one so it does add absolute complexity. But the golang one is relatively more complex, because the Ruby version uses higher-level equivalents, and those abstractions are good enough that I don't ever have to actually reason about what happens underneath the covers. Put another way, even the golang version is an abstraction around an absolutely massive amount of hardware and electrical engineering complexity that you virtually never have to think about. The absolute complexity of what happens when you compile and execute those instructions is extreme to the point that no single human or even room of humans collectively fully knows what's going on. And yet we manage just fine. Or, I love this example: admin_usernames = users.select(&:admin?).map(&:username) Versus adminUsernames := make([]string, len(users)) for i, user := range users { if user.isAdmin { userNames = append(userNames, user.Name) } } Again, spot the bug!
- azornathogron 5y agoTwo things... I think (?) - declaring `adminUsernames` and then using `userNames` (but I assume that's not what you're talking about because that won't even compile) - you're making an array with its length N instead of length 0 capacity N (I totally agree with your point, I just like looking for bugs)
- stouset 5y agoYep, the `userNames` thing was my own dumb mistake while editing in a comment box and was not intended as a reflection of the language. The capacity/length issue is the actual bug I was intending to include. But... also it's a mistake that's not even possible in the Ruby example due to not having to handle the "irrelevant" internal details of appending to the array, so maybe there's a point to it after all.
- jatone 5y ago
- CraigJPerry 5y agoGenerally direct refutation of a central point is a constructive argument. Here’s another example of direct refutation: You’ve given a strawman argument, specifically you’ve given one implementation which has abstracted the details (we don’t see the code for take, select and reduce). That’s just an arbitrary decision you’ve made, the equiv go example you could have posted might be: // idiomatic error handling elided only for brevity first20, err := Take(nums, 20); odds, err := Select(first20, Odd); sum, err := Sum(odds); You’ve presented these different levels of abstraction and then argued against a point that wasn’t made. A strawman argument. In the interest of steelman-ing your argument, the interesting difference would be in the comparison of implementations of take() or select() or reduce() - but ruby is a dynamic language so there’s not really a comparison to be made. We can still say how we might approach Take() or Select() or Reduce() or Sum() though if we need them to be generic over argument types - in the absolute worst case (so not using go generate to help us here or an interface or the new generics functionality) in the worst case e we would have repeated definitions of these functions. Code that any junior developer will be able to safely reason about and change. Code that has utterly obvious risks (you might introduce a differing behaviour in one implementation of Take() for example) - so obvious that it’s trivial to defend against with nothing more than generative testing. Again, painfully simple code. Zero cleverness. Any developer of any experience level can quickly make a valid change.
- int_19h 5y ago> in the worst case we would have repeated definitions of these functions. Code that any junior developer will be able to safely reason about and change It's also code that many junior developers will forget to change in all the places when they fix a bug in one of them.
- CraigJPerry 5y agoYeah that’s what i was getting at with: >> Code that has utterly obvious risks (you might introduce a differing behaviour in one implementation of Take() for example) - so obvious that it’s trivial to defend against with nothing more than generative testing. Really obvious risks are usually easier to handle than more obscure ones.
- pipe_connector 5y agoI legitimately cannot spot the bug. Can you write out the equivalent Ruby code in Go pre-generics? I really did try but I honestly cannot understand what your Ruby code is supposed to do. I don't know what select and take are supposed to do, and I can guess at reduce but the symbols in the method are utterly arcane to me. Does Take grab exactly 20 items or does it do something like nums[:21]? Depending on how Take is implemented it could be an off-by-one error in your loop? Usually in this case I'd check for equality instead; that's the only thing that looks off to me in your Go code. In my opinion the Go code is dead simple and unambiguous with no nuance hidden away in methods whose exact semantics I certainly don't have memorized. I'd have to look at 3 different function signatures to figure out what those Ruby methods do every single time I was reviewing code like this.
- stouset 5y agoThe code is equivalent, except the golang version accidentally loops an extra time. `&:meth` is just Ruby syntax for "a function that calls the `meth` method on its argument". So `&:foo` is shorthand for `func(x) { x.foo }`. `take(n)` just returns the first `n` elements. `select(fn)` iterates for every element for which the function passed to it returns true. `reduce(fn)` combines the previous iteration with the result of calling the provided function on the next iteration. Thus `take(20)` gets the first 20 elements, `select(&:odd?)` iterates over every odd value in what's left, and `reduce(&:+)` adds every remaining element together. > In my opinion the Go code is dead simple and unambiguous with no nuance hidden away in methods whose exact semantics I certainly don't have memorized. These functions and Ruby's `&` syntax took a few sentences to explain. They are fundamentally not that complicated, although nobody would expect you to understand what they did before you've seen them just like any other function call. You didn't know what they do, which is fine. But now you do, and it is completely normal for people to assume that you should be capable of working with them in the same way that someone would expect you to write 2 * 5 instead of writing 2 + 2 + 2 + 2 + 2. Writing the full `for` loop in my example is the equivalent of the latter, with the equivalent pitfall that it's really easy to accidentally write an extra sixth addition when you only meant to put five of them. > In my opinion the Go code is dead simple and unambiguous with no nuance hidden away in methods whose exact semantics I certainly don't have memorized. I'd have to look at 3 different function signatures to figure out what those Ruby methods do every single time I was reviewing code like this. No, you wouldn't, for the exact same reason you don't look up the documentation for `for` or `range` or `if` or `*` every time you use them. They are simple, straightforward abstractions that any programmer will have more or less fully internalized given fifteen minutes of playing around with them, and which you will likely use multiple times a day in any language that supports them. I can assure you that bordering on 0% of programmers who regularly use languages with these functions did not understand the example. There was a time when I, too, did not know what to make of functions like these. I saw someone write a similar comment and I thought "that's totally inscrutable". And then I learned what those functions do and I now use them almost literally every single day. They are amongst the most useful and universal abstractions I have ever come across.
- morelisp 5y agoThere are two bugs in the code, and your (intentional?) use of non-idiomatic Go is the cause of one of them. Since you don't mention it in your follow-up comment I assume you didn't mean to write it in the first place. Still, there is a middle ground that remains both efficient and readable: sum := 0 for _, v := range take(20, nums) { if v % 2 == 1 { // or if odd(v), if you like sum += v } } I'd venture most of the clarity come from the generic take (specifically, being able to implicitly take min(len(nums), 20)), not the generic filter/reduce. The second point of clarity comes from Ruby's dynamic method dispatch and large method set on numeric types; no thanks. Only as a third-tier aspect does select/reduce come into play, and I think that one is much more questionable (e.g. reduce or fold, and either way how do I explain this name to new programmers? -- and &:+, what a messy identifier).
- dorianmariefr 5y agoFor those that want to test the code (1..100).take(20).select(&:odd?).sum
- dorianmariefr 5y agoI think the bug is that it sums even numbers and not odd numbers, e.g. `v % 2 == 1 { continue }`
- cellularmitosis 5y ago> As a general rule, if you're referencing the dictionary definition of a word to make your point, you're just playing semantic games. Before dismissing this as silly semantic games, you should watch the talk which they were very likely referencing: https://www.infoq.com/presentations/Simple-Made-Easy/ https://www.infoq.com/presentations/Simple-Made-Easy/
- yencabulator 5y agoHave you thought about the amount of allocations and copying your chained generic map-reduce thing would cause? Go for loops are good because they are simple, even if you have to use more than one line.