Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I feel like, at least in some cases, the article is going out of its way to make the "undesired" look worse than it needs to be. Compairing

    fn get_ids(data: Vec<Widget>) -> Vec<Id> {
        collect(map(filter(map(iter(data), |w| w.toWingding()), |w| w.alive), |w| w.id))
    }
to

    fn get_ids(data: Vec<Widget>) -> Vec<Id> {
        data.iter()
            .map(|w| w.toWingding())
            .filter(|w| w.alive)
            .map(|w| w.id)
            .collect()
    }
The first one would read more easily (and, since it called out, diff better)

    fn get_ids(data: Vec<Widget>) -> Vec<Id> {
        collect(
            map(
                filter(
                    map(iter(data), |w| w.toWingding()), |w| w.alive), |w| w.id))
    }
Admittedly, the chaining is still better. But a fair number of the article's complaints are about the lack of newlines being used; not about chaining itself.


In my eyes newlines don't solve what I feel to be the issue. Reader needs to recognize reading from left->right to right->left.

Of course this really only matters when you're 25 minutes into critical downtime and a bug is hiding somewhere in these method chains. Anything that is surprising needs to go.

IMHO it would be better to set intermediate variables with dead simple names instead of newlines.

fn get_ids(data: Vec<Widget>) -> Vec<Id> {

    let iter = iter(data);

    let wingdings = map(iter, |w| w.toWingding());

    let alive_wingdings = filter(wingdings, |w| w.alive);

    let ids = map(alive_wingdings, |w| w.id);

    let collected = collect(ids);

    collected

}


> Reader needs to recognize reading from left->right to right->left.

Yeah, I agree. The problem is that you have to keep track of nesting in the middle of the expression and then unnest it at the end, which is taxing.

So, I also think it could also read better written like this, with the arguments reversed, so you don't have to read it both ways:

  fn get_ids(data: Vec<Widget>) -> Vec<Id> {
      collect(
         map(|w| w.id,
             filter |w| w.alive,
               (map(|w| w.toWingding(), iter(data)))))
  }
That's also what they do in Haskell. The first argument to map is the mapping function, the first argument to filter is the predicate function, and so on. People will often just write the equivalent of:

  getIDs = map getID . filter alive . map toWingDing
as their function definitions, with the argument omitted because using the function composition operator looks neater than using a bunch of dollar signs or parentheses.

Making it the second argument only makes sense when functions are written after their first argument, not before, to facilitate writing "foo.map(f).filter(y)".


I've been prototyping a programming language[0] with Haskell-like function conventions (all functions are unary and the "primary" parameter comes last). I recently added syntax to allow applying any "binary" function using infix notation, with `a f b` being the same as `f(b)(a)`[1]. Argument order is swapped compared to Haskell's infix notation (where `a f b` would desugar to `f(a)(b)`).

Along with the `|>` operator (which is itself just a function that's conventionally infixed), this turns out to be really nice for flexibility/reusability. All of these programs do the same thing:

  1 - 2 - 3 + 4

  1
    |> -(2)
    |> -(3)
    |> +(4)

  +(4)(
    -(3)(
      -(2)(1)
    )
  )
It was extremely satisfying to discover that with this encoding, `|>` is simply an identity function!

[0]: https://github.com/mkantor/please-lang-prototype

[1]: In reality variable dereferencing uses a sigil, but I'm omitting it from this comment to keep the examples focused.


The argument ordering Haskell (and, I think, most functional languages) uses is definitely simpler to read. It keeps the components of the tranformation/filter together.


Oh wow, are we living in the same universe? To me the one-line example and your example with line breaks... they just... look about the same?

See how adding line breaks still keeps the `|w| w.alive` very far from the `filter` call? And the `|w| w.id` very far from the `map` call?

If you don't have the pipeline operator, please at least format it something like this:

    fn get_ids(data: Vec<Widget>) -> Vec<Id> {
        collect(
            map(
                filter(
                    map(
                        iter(data),
                        |w| w.toWingding()
                    ),
                    |w| w.alive
                ),
                |w| w.id
            )
        )
    }
...which is still absolutely atrocious both to write and to read!

Also see how this still reads fine despite being one line:

    fn get_ids(data: Vec<Widget>) -> Vec<Id> {
        data.iter().map(|w| w.toWingding()).filter(|w| w.alive).map(|w| w.id).collect()
    }
It's not about line breaks, it's about the order of applying the operations, and about the parameters to the operations you're performing.


> It's not about line breaks, it's about the order of applying the operations

For me, it's both. Honestly, I find it much less readable the way you're split it up. The way I had it makes it very easy for me to read it in reverse; map, filter, map, collect

> Also see how this still reads fine despite being one line

It doesn't read fine, to me. I have to spend mental effort figuring out what the various "steps" are. Effort that I don't need to spend when they're split across lines.

For me, it's a "forest for the trees" kind of thing. I like being able to look at the code casually and see what it's doing at a high level. Then, if I want to see the details, I can look more closely at the code.


> The way I had it makes it very easy for me to read it in reverse; map, filter, map, collect

Yes, sure, who cares. But the way you wrote it, it's impossible to match those "map, filter, map, collect" to their parameters: `, |w| w.toWingding()), |w| w.alive), |w| w.id))`. Impossible!

You just include all the parameters to the various function calls on one very long line! To top it off, it's the most indented line that you decide to make the longest! If I could I'd put you in jail for this!


They did touch on that.

> You might think that this issue is just about trying to cram everything onto a single line, but frankly, trying to move away from that doesn’t help much. It will still mess up your git diffs and the blame layer.

Diff will still be terrible because adding a step will change the indentation of everything 'before it' (which, somewhat confusingly, are below it syntactically) in the chain.


Diff can ignore whitespace, so not really an issue. Not _as_ nice, but not really a problem.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: