Hacker News new | ask | show | jobs
by stouset 1254 days ago
I'm sorry, but this perspective is absolutely bizarre to me.

We should not avoid language features that reduce boilerplate and drastically increase comprehension for for people who have experience in a language in order to cater to people who have minimal or no experience in that language.

Should we not use `?` in Rust because it might be obscure to someone who's never used the language? Should we not use any of the `Iterator` functions (including `zip` and `map`) because they might be confusing to C programmers who only know `for` loops?

Functional concepts are everywhere these days. Most of them are not hard. `zip` and `map` do not require understanding of homotopy type theory to understand, they are essentially trivial functions. They are available across every type you could possibly iterate over in Rust, and if you understand what they do on one of those you essentially understand what they do on all of them.

This is a toy example, but virtually every piece of hard evidence we have in this field shows that—within reason—more concise code has fewer bugs and is easier to comprehend than longer, more verbose equivalents. Writ large across a project, doubling or tripling the amount of code to bend over backwards accommodating complete novices is lunacy.

2 comments

I half agree. The question mark version seems to be good as well, in fact also more idiomatic.

If the zip/map version is more common then I take everything back. But it seems less malleable and clear than the pattern matching examples. I had to stop and think for a second to get it. I find pattern matching in general more declarative for small amounts of items.

---

In terms of preferring clearer and simpler code: Absolutely yes. I avoid unnecessary abstractions, especially if the gains are so minor or questionable. It's a matter of empathy and foresight.

Whether that's the case here: I don't know. It might very well be that this is common and clear in the Rust world.

Every abstraction is unnecessary. And the gains are almost always minor… until you apply some of those abstractions across an entire code base.

C-style `for` loops were the norm for decades. Now virtually every language gives you some ability to iterate directly over every element in a collection. Replacing a `for` loop with an iterator over each element is never necessary. The old way worked for decades. The gains are minor. Should we go back to C-style `for` loops? If not, why not?

When you understand the answer to that, you’ll understand why that same logic applies to trivial functions like `zip` and `map` that simply take the idea one minor step further.

I’m not arguing against abstraction in general. I argue that clarity comes first and that in this specific instance pattern matching seems more clear to me.
Sorry, this is just a pet peeve of mine in general.

All the time I see people argue against “unnecessary” abstractions. But this almost always comes from the perspective of “I don’t personally understand it yet” which is just not a reasonable bar for anything to have to clear. Second most frequently it’s “less clear” which often just means “I haven’t internalized it yet” which is likewise a terrible evaluation method for something like functional iteration methods that have the possibility of being used virtually everywhere. And almost equally as often, the underlying objection is that it isn’t useful because the old way is just fine thanks, which is typically a perspective that has completely forgotten about all of the sharp edges and bugs that we all just grew to accept from the preexisting approach.

All of these types of objections are knee-jerk reactions. There are good arguments against bad abstractions, leaky abstractions, infrequently-used abstractions, overly-complex abstractions, and all sorts of other failures to abstract well. But people are so used to these that they reflexively oppose any new abstraction as overly complex or unnecessary simply because it’s new to them.

And that’s a terrible perspective to have, because quite literally all of the progress that has ever been made in the practice of software engineering has been due to abstraction.

The `zip` and `map` functions used here are actually functions of the module std::option, and not std::iter. While they are the same idea "in essence", they have different implementation. The std::option ones are a simple pattern match, while the std::iter ones are more complex. For example, std::iter::zip returns an std::iter::Zip, while std::option::zip returns an Option of a tuple.

I'll also add that option's zip and map are also implemented with a pattern match, like above.

One error and one deviation from the established norm for a toy example is a lot. At the scale of a codebase it would be a catastrophe.

Yes, I know.

But if you look closely you'll notice that `zip` and `map` were called directly on an array here and not actually on an iterator. That's a third implementation of the same concepts. If Rust had HKTs they could all be the exact same implementation, but not today.

The important thing, though, is that they all conceptually do the same thing. Understanding one essentially translates to understanding them all. If zip/map are called directly on two Options, you get an Option containing a tuple back out. If they're done on two arrays, you get an array containing tuples back out. If they're done on two iterators, you get an iterator containing tuples back out.

There's nothing to be confused about.

> But if you look closely you'll notice that `zip` and `map` were called directly on an array here and not actually on an iterator.

No, I don't think that's true, unless we're talking about two different things. In the article, and in the following post https://news.ycombinator.com/item?id=34428999, zip is user on an Option<i32>, takes another Option<i32>, return an Option<(i32, i32)> (which is a tuple, not an array), on which map is applied to extract the two values and add them.

> If zip/map are called directly on two Options, you get an Option containing a tuple back out. If they're done on two arrays, you get an array containing tuples back out. If they're done on two iterators, you get an iterator containing tuples back out.

But that's my whole point. std::option::map is not the same function as std::array::map, which is not the same function as core::iter::Iterator::map. One big difference, for example, is that core::iter::Iterator::map is lazy, while the others are not, hence the note to try to avoid chaining std::array::map, and being careful around it in performance-critical code: https://doc.rust-lang.org/src/core/array/mod.rs.html#466.

Even with HKTs, while you could share some code, that wouldn't solve the fact that the "direct map" (std::option::map for example) is strict, and the other map (std::option::iter::map) is lazy. Especially in a language often used for performance-sensitive tasks, I can't agree that understanding one map translates to understanding them all, since that would be ignoring part of their ergonomics, and more importantly their performance characteristics.