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

It looks like I'm on the minority here, but I generally like Rust's syntax and think it's pretty readable.

Of course, when you use generics, lifetimes, closures, etc, all on the same line it can become hard to read. But on my experience on "high level" application code, it isn't usually like that. The hardest thing to grep at first for me, coming from python, was the :: for navigating namespaces/modules.

I also find functional style a lot easier to read than Python, because of chaining (dot notation) and the closure syntax.

Python:

    array = [1, 0, 2, 3]
    new_array = map(
        lambda x: x * 2,
        filter(
            lambda x: x != 0,
            array
        )
    )
Rust:

    let array = [1, 0, 2, 3];
    let new_vec: Vec<_> = array.into_iter()
        .filter(|&x| x != 0)
        .map(|x| x * 2)
        .collect();
I mean, I kind of agree to the criticism, specially when it comes to macros and lifetimes, but I also feel like that's more applicable for low level code or code that uses lots of features that just aren't available in e.g. C, Python or Go.

Edit: Collected iterator into Vec



There are people who write Python code like that, but it's an extreme minority. Here's the more likely way:

    array = [1, 0, 2, 3]
    new_array = [x * 2 for x in array
                 if x != 0]
Just as a matter of style, few Python programmers will use lambda outside something like this:

    array = [...]
    arry.sort(key=lambda ...)


i have always felt the backwards nature of list comprehensions makes them very hard to read


me too. Its one of the things that I kinda dislike in Python.


I guess you're right, list/generator comprehensions are the idiomatic way to filter and map in python, with the caveat of needing to have it all in a single expression (the same goes for lambda, actually).

I still feel like chained methods are easier to read/understand, but list comprehensions aren't that bad.


Even in Rust I don't like chains that go beyond ~4 operations. At some point it becomes clearer when expressed as a loop.


> with the caveat of needing to have it all in a single expression (the same goes for lambda, actually).

one could use multiple expressions in lambda in (modern) Python


Do you mean using the walrus operator? Because unless I missed a recent PEP, I don't know of a way to do this without something hacky like that.


yes

x = 1 y = 2

q = list(map(lambda t: ( tx := tx, ty := ty, tx+ty )[-1], [1, 2, 3]))

print(q)


Guido van Possum has expressed distaste for list comprehension. Take that for what it's worth.

https://news.ycombinator.com/item?id=13296280


I realize that "Guido van Possum" was almost certainly a typo here, but it _does_ make for an amusing mental image. I wonder what other forest creatures might have built programming languages? C++ built by Björn "the bear" Stroustrup? C# built by Anders Honeybadger? Ruby by Yukihiro "Catz" Cat-sumoto?


Indeed an autocorrect. But it was definitely Graydon Boar who created Rust


From the link, it sounds like GvR has expressed distaste for functional programming idioms like map/reduce, but not for list comprehensions.

At least it's not Go's "You gonna write a for loop or what?"


He was also against having any kind of lambda, and the one line version was the concession he was willing to let in.


1. I don't think your Python example if fair. I think

    new_array = [x*2 for x in array if x != 0]
is much more common.

2. In your example, `new_array` is an iterator; if you need to transform that into an actual container, your rust code becomes:

   let new_array = array.into_iter()
        .filter(|&x| x != 0)
        .map(|x| x * 2)
        .collect::<Vec<_>>();
And there your generic types rear their ugly head, compared to the one liner in python.


Oh, yeah, you're right! If you want to collect into a Vec you may need to specify the type, but usually, you can just call `.collect()` and the compiler will infer the correct type (as I suppose you're collecting it to use or return).

If it can't infer, it's idiomatic to just give it a hint (no need for turbofish):

    let new_vec: Vec<_> = array.into_iter()
        .filter(|&x| x != 0)
        .map(|x| x * 2)
        .collect();
I don't think that's ugly or unreadable.

About the Python list comprehension, I answered your sibling, I think you're both right but it also does have it's limitations and that may be personal, but I find chained methods easier to read/understand.


They maybe rear their ugly head but they also allow you to collect the iterator into any collection written by you, by the standard library or by any other crate.

While in python you have list/dict/set/generator comprehension and that's it.


I don't think it's bad thing. In fact one of my favorite features is that you can do `.collect::<Result<Vec<_>, _>>()` to turn an interators of Results, into a Result of just the Vec if all items succeed or the first error. That is a feature you just can't express in Python.

But you have to admit that is a pretty noisy line that could be difficult to parse.


Then don't write it that way.

   let new_array: Vec<usize> = array.into_iter()
        .filter(|&x| x != 0)
        .map(|x| x * 2)
        .collect();
Isn't so bad.


I believe I have the habit of putting it on the end because the final type might be different. Consider:

    let array = ["DE", "AD", "BE", "EF"];
    let new_array: Vec<u32> = array.into_inter()
      .map(|x| u32::from_str_radix(x, 16))
      .collect()?;
In this case you need to specify the Result generic type on generic. This has come up for me when working with Stream combinators. Most projects probably end up in needing some lifetime'd turbofish and you have to be able to parse them. They aren't rare enough, IME, to argue that Rust isn't noisy.


I generally just write it like this:

   let new_array: Vec<_> = array.into_iter()
        .filter(|&x| x != 0)
        .map(|x| x * 2)
        .collect();
I'm actually a bit confused by the `&x` given that `into_iter()` is used, which would take ownership of the array values, but assuming that it was supposed to be just `iter()` (or that it's an array of &i32 or something I guess), you're going to be copying the integer when dereferencing, so I'd probably just use `Iterator::copied` if I was worried about too many symbols being unreadable:

   let new_array: Vec<_> = array.iter()
        .copied()
        .filter(|x| x != 0)
        .map(|x| x * 2)
        .collect();
There's also `Iterator::filter_map` to combine `filter` and `map`, although that might end up seeming less readable to some due to the need for an Option, and due to the laziness of iterators, it will be collected in a single pass either way:

   let new_array: Vec<_> = array.iter()
        .copied()
        .filter_map(|x| if x == 0 { None } else { Some(x * 2) })
        .collect();
This is definitely more verbose than Python, but that's because the syntax needs to disambiguate between owned and copied values and account for static types (e.g. needing to annotate to specify the return type of `collect`, since you could be collecting into almost any collection type you want). It's probably not possible to handle all those cases with syntax as minimal as Python, but if you are fine with not having fine-grained control over that, it's possible to define that simpler syntax with a macro! There seem to be a lot of these published so far (https://crates.io/search?q=comprehension), but to pick one that supports the exact same syntax in the Python example, https://crates.io/crates/comprende seems to do the trick:

    let array = [0, 1, 2, 3];
    let new_array = c![x * 2 for x in array if x != 0];
    println!("{:?}", new_array); // Prints [2, 4, 6]
I'm not trying to argue that Rust is a 1:1 replacement to Python or that if Python suits your needs, you shouldn't use it; I think it's worth pointing out that Rust has more complex syntax for a reason though, and that it has surprisingly good support for syntactic sugar that lets you trade some control for expressiveness that can alleviate some of the pain you might otherwise run into.


It actually needs the & in both my example and your second example, because .filter receives a reference to the item being iterated. Your second example doesn't compile: https://play.rust-lang.org/?version=stable&mode=debug&editio...


Ah, you're right! I had forgotten filter worked like that. I'll have to concede that equality with references to `Copy` is somewhat annoying when dealing with closures (and closures in general are one of the few parts of Rust I do consistently wish had better ergonomics, although I understand why it's hard).


You can just specify the type along with the variable itself instead of relying on type inference in this case, which makes it look a lot better I think.

There is also the collect_vec method from Itertools that avoids this. I normally am not a big fan of pulling crates for little things like this, but the Itertools crate is used in rustc itself, so you already are trusting it if using rust.

I do agree that rust syntax can be a bit verbose sometimes but I actually prefer the syntax to most other languages! I would have preferred if it would have been less inspired by the C family of languages syntax wise, but that would have likely hindered adoption.


I think part of this comes down to: Does your Rust code make heavy use of generics? I find myself deliberately avoiding generics and libraries that use them, due to the complexity they add. Not just syntactic noise, but complicated APIs that must be explicitly documented; rust Doc is ineffective with documenting what arguments are accepted in functions and structs that use generics.

See also: Async.


> See also: Async.

Ah, the good ole generic, nested, self-referencing, unnameable state machine generator.


I want to like rust but the fact that there are replies with 9 different ways to write that loop posted here alone, and no consensus on which one is the idiomatic, is not a good sign.


minor point but your python code creates a generator here not an array, you'd have to wrap it in a `list()` to get the same data type and be able to for example assert its length (of course you can just iterate over the generator)


You can only do this chaining when everything happens to be a method. Compare to Nim or D, where you can chain arbitrary procedures like this.




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

Search: