7 ms·
`.map().filter().find(x)` is significantly more simple and readable than for-looping for every small thing in Go.
by dmitriid 6y ago
`.map().filter().find(x)` is significantly more simple and readable than for-looping for every small thing in Go.
- throwaway894345 6y agoI disagree. Fewer characters != simpler or even more readable. Map/filter/etc are certainly more clever, and I enjoy writing clever code, but I reserve it for my personal projects bprecisely because it isn’t so clear. Note that Python has these first class functions as well as various kinds of comprehensions and still it’s frowned upon to use anything more complex than the simplest comprehensions.
- creata 6y agoAlright, if it's not too much trouble, can you show me what the equivalent of the following code would be in Go? I think this is extremely clear as-is, but I'd love to see how you can apparently make it even clearer by avoiding iterator combinators. https://play.rust-lang.org/?version=nightly&mode=release&edition=2018&gist=fbb03d43417b13234c9146c6a205f6c2 https://play.rust-lang.org/?version=nightly&mode=release&edi...
- throwaway894345 6y agoPersonally I think this is clearer, but it's quite contrived either way: https://play.golang.org/p/FpUWwm5yZ8n https://play.golang.org/p/FpUWwm5yZ8n
- creata 6y agoNice! Out of curiosity, if the iterators weren't of the same type (e.g. one was an iterator over the values of a hashmap and the other was over the items in a list) what would you have used instead of for _, m := range maps { ... }?
- coder543 6y agoIf you could provide the hypothetical Rust code you're referring to, that would probably make your question more clear here. I know both Rust and Go rather well, and I'm confused about which direction you're wanting to take the code.
- throwaway894345 6y agoJust as with Rust, you'd have to convert both maps to the same type before you could iterate them over together (or chain them, in Rust).
- jolux 6y agoMap and filter are extremely simple, far simpler than a for-loop. They can only do two things: transform items in a collection, and remove items from a collection. After mapping you know for sure that a collection will still have the same number of elements. After filtering you know that all the elements remaining still have the same type. For-loops can do anything. It’s a question of what you’re most familiar with and used to.
- throwaway894345 6y agoI’m not comparing for loops in general with map or filter, but rather the specific for loop boilerplate to implement map and filter. Moreover, the complexity of map/filter isn’t internal, but rather decomposing or breaking down one’s problem into map/filter/etc expressions. Often for loops are clearer, especially since you can frequently collapse a map and filter into a single for loop without thinking in map/filter/etc primitives. This is especially true when you need to abort the chain early (e.g., due to errors)—Rust’s FromIterator provides a clever solution to this problem, but it’s much harder to wrap one’s head around than a simple for loop with an early return branch.
- jolux 6y agoI find that decomposing iteration into map and filter expressions typically clarifies my understanding of the problem and makes the resulting code more general too, and I just disagree that for-loops are clearer. Combining a map and filter into one loop body tends to create specific, imperative code that doesn’t need to be specific or imperative. For one, loops require mutation to have the same effect as filter and map, and as soon as you’re maintaining state, things get confusing. My usage of higher order functions instead of for-loops is an expression of my belief that programmers should always use the least powerful construct they can to solve a problem. In general though I think the functions used in map and filter should be total. Most languages are not expressive enough to handle errors fluidly in long chains like that, you can do it with monads but apparently everyone hates those except me.
- throwaway894345 6y ago
- joelfolksy 6y agoThis argument doesn't really have any content, because people who advocate for programming with higher order functions would not agree that such code is more clever; in fact, they would probably argue the opposite. What's the point of debating if you assume the people on the other side already agree with you?
- patates 6y agoI really like chaining array methods but you should also never forget it's incredibly easy to introduce crazy computational complexity with them.
- coder543 6y agoRust has lazy iterators, so a chain of iterator steps like this has the same complexity as a single for loop over the iterator. There's no reason Go couldn't do that same thing.
- stouset 6y agoIn fact, Rust's version compiles down to pretty much the optimal looping form. It will even vectorize your iterations using SIMD.
- throwaway894345 6y agoRust is seriously cool in this regard, but Go aspires to be different, namely more C-like and less C++-like. This means a dumber, faster compiler that is easier to intuit about, for better or worse. Rust is such an amazing piece of engineering (moreso an amazing process and culture for producing an amazing language), but I think Go’s approach is right for a whole lot of software—lots of things don’t need to be insanely performant (lots of things are chugging along in Python, which runs 2-3 orders of magnitude slower than Go) but we do need to be able to iterate on them quickly in a development context involving dozens or hundreds of other people.
- jolux 6y agoNot if you have solid lazy iterator support in the language. If they’re strict and every link in the chain is adding O(n) to the complexity then yes, this is a problem.
- twobitshifter 6y agoIn the case we’d need a little context of what the map, filter, and find operations are, but if map is multiple by 2 and filter is less than 200 you can just replace the filter and map with a single if in the for loop that performs the multiplications and check and return immediately when you find X. That’s pretty obvious, always takes only one run, doesn’t reallocate, and doesn’t add concepts, which is of course adding complexity.
- stouset 6y agoYes and we could replace all of that with an implementation in assembly which both compiles and runs faster. But we don't do that because for the entire history of computing, we've come to realize that abstractions enable us to reason about programs at a higher level without having to be bogged down in irrelevant details and that getting those details right once lets us avoid the inevitable bugs that occur while reimplementing them thousands of times. By absolutely every objective measure we know in software engineering let triples = ints.map(|x| x * 3) is strictly superior to var triples = []int for value := range ints { triples = append(triples, value * 8) } The fact that this even has to be argued is just bonkers to me. The second one is trivial to introduce bugs into, reading it requires mentally filtering out 90% of the code as irrelevant minutiae, and it's even worse from a performance perspective unless you remember each and every time to allocate a result array of the appropriate length to avoid reallocation. None of the details of allocating a result array or iterating over indices/values are important from the purpose of expressing the problem to be solved, and yet every time someone has to read this solution those things are of greater visibility than the actual business logic. Worse, repetitive and unnecessary boilerplate like this hides bugs. Did you catch the fact that I'm multiplying by the wrong value in the golang version? Did you catch that I'm accidentally tripling the indices and not the values themselves? If you did, would you at least acknowledge that the first bug is much easier to identify in the first example than the second, and that the second bug is strictly impossible? As an added bonus, the first one (in Rust at least) is automatically vectorized using SIMD for optimal performance. This happens even as you chain additional operations to arbitrary complexity. If you still somehow think the second version is "better", then whatever your argument is can trivially be repurposed to say this version is better still: int* triples = malloc(sizeof(int) * array_len); for (int i = 0; i < array_len; i++) { triples[i] = array[i] * 3; } If those details of creating a result array and explicitly enumerating are important, why isn't maintaining your own counter and directly indexing? At least the C version somewhat encourages you to allocate a result array of the correct size.