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.
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.
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.
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?
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.
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.
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.
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.
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)
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:
Rust: 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