> BUT when you try to use it with iterators -- which are also amazing, I love using iterators -- IT DOESNT WORK.
Yes it does
let res: Result<Vec<_>, _> = iterator.iter().map(|x| x.foo()).collect();
let res = res?;
> But this also happens elsewhere, because of this ugly inflexability, we cant do:Use and_then:
let z = x.foo().and_then(|y| y.bar())?;
I get that there's an upfront cost to learning this stuff, but I think he's blaming the language a little too aggressively.This is a pretty low quality rant. Going example by example:
for (i, x) in something.iter_mut().filter(|| {...}).enumerate() {
*x = (*x) * i
}
you can this this in loop style if you want: for i in range(something.len()) {
let x = &mut something[i];
if ... {
*x = (*x) * i;
}
}
or if you prefer, in iter style: something
.iter_mut()
.filter(|| {...})
.enumerate()
.for_each(|(x, i)| *x = (*x) * i);
The next two examples can be written: let res: Vec<_> = iterator.iter().filter_map(|x| x.foo()).collect();
let z = x.foo().and_then(|y| y.bar());
There is an ergonomics issue around the fact that if you stick `?` or `.await()` in a closure, it returns to that closure instead of the function containing it. But the way this issue manifests itself is the standard library having a pile of different variations on methods, like `.map()` vs. `.filter_map()`. That's the thing to complain about, not the fact that closures work the same way they do in every other language.> the for loop syntax should be simple syntactic sugar for iterator.iter().for_each(BODY), not bespoke syntax. The fact this breaks wrecks my mental model.
I'm sorry to be blunt, but this mental model is wrong. .for_each runs a function, while for does not.
If what you want is to return early on an error, you can .collect() an Iterator<Item=Result<T, E>> directly into a Result<Vec<T>, E> and then ? that. That's the equivalent of for and ?. Or, you can collect into a Vec<Result<T, E>>.
> You must use the for loop syntax if you want to use .await, because iterators are not powerful enough to support real world use-cases.
Given that what this requires is async closures, and async closures are an active area of work, this seems like a somewhat unhelpful comment.
I definitely agree about having better syntax for iter_mut, though.
Look, it's easy to beat up on an article like this and feel good and smug about it.
...but, let's not beat around the bush. Rust has some sharp edges (1).
Why can't you clone a boxed function? I get it, Box<dyn Foo> isn't sized, so you can't clone it. Ok! ...but you can clone a closure because a raw closure is clone if the contents is clone. Not when it's boxed though.
Why cant you express that a struct can contain an object and a reference to the object and have a 'static lifetime?
Yes, you can collect::<Result<...>>, but why can't you just use the `?` sugar for it? There's only one possible reason to end a collect() with ? surely?
Yup. I get it.
Lot of reasons, writing languages is hard.
...but, I dunno. I read this:
> the teams working on language design need to SLOW DOWN and focus on ergonomics and composability.
...and I think about `trait ?async` and I read the roadmap (2), and I have to say, I'm sympathetic. More sympathetic then I should be, perhaps, given how ragey the article is... but still.
I feel that pain too.
I use rust because it's nicer than C++. If it turns into a trash can of features (like C++), why bother?
[1] - https://stackoverflow.com/questions/tagged/rust?tab=Frequent [2] - https://rust-lang.github.io/async-fundamentals-initiative/ro...
> But the teams working on language design need to SLOW DOWN and focus on ergonomics and composability. Not adding new syntax because some other language has it.
Genuinely when was the last time Rust actually added new syntax to the language? I think the try operator and await syntax were both added in 1.39 back in 2019. Const generics was added in Feb 2021.
Generic associated types were added recently in November I think, but that's not really added syntax so much as removing a restriction on generics.
From the first few graphs:
> Rust has a nice pretty syntax for iterating:
for x in &mut something {
*x = (*x) * 2;
}
> EXCEPT when you need to do anything else to the iterator, then its ugly: for (i, x) in something.iter_mut().filter(|| {...}).enumerate() {
*x = (*x) * i
}
This is just so goofy. Who writes Rust like this? Wouldn't everyone write: something
.iter_mut()
.filter(|| {...})
.enumerate()
.for_each(|(i, x)|) {
*x = (*x) * i
});
> There should be one -- and preferably only one --obvious way to do it.Overhead-wise, you can't expect everyone to take to the iterator model right away. Sometimes you want a for loop, or you want a for loop for right now.
I think Rust being multi-paradigm is a strength, with the understanding that the preferred approach/model is the more functional, more immutable iterator model for 95% of your use cases.
> Again, the absolute lack of composability is astounding. Whats the point of even having iterator methods if you can't use them for real world usecases, where code is regularly fallable, so you need to return a Result.
You can, you just haven't figured out how yet. This is frustrating, but, gosh, you'll learn how sooner or later.
let y: Result<Vec<_>> = iterator.map(|x| {
x.some_fallible_method()
}).collect();
Reads like someone used to working in an abstract high-level language, who doesn't like the verbosity mandated by a language aimed squarely at embedded/systems programming.
Maybe one day there will be a language that has the expressive type system of ML, the garbage collection of ML, and the predictable performance of ML. Then people who don't need to care about the stack layout differences of an async function can use that language instead of Rust.
I don't think it is surprising. Nested functions do not capture the return (and yield) continuation of their containing function, so they can't invoke it. Rust is in good company here and I think it is the right default for lambdas.
In principle you could have a variant scoped function syntax that does that, but that will complicate both the language implementation and syntax
The reason is that `map`, `for_each` and the rest are not syntax and take closures. Closures in any language do not affect the control flow of their containing function.
These two annoyances could be resolved with:
- async iterators https://github.com/rust-lang/rust/issues/79024
- an extension trait for iterators over results: https://crates.io/crates/iterr
I don't get it.
Why most "modern" languages are using such weird/contrived and hard to read syntax?
I can understand that C++ evolved over a long time, starting from C backward compatibility, I think mistakes were made but I can imagine the constraints.
But for Rust and Zig, they started from scratch, why do they have to use so many sigils (magic symbols)?
System programming should not look like sed or Perl.
It would help to have more complete examples here. Also, at least one of the examples that are supposed to be correct clearly does not compile (the loop calling push will fail because the Vec is not declared as mut), which makes me doubt the assertions about the other examples, especially because of the lack of context.
> for x in &mut something { ⌠}
The thing to realise is that this largely isnât how youâll use the <&mut Something as IntoIterator> implementation. Itâs not ââprettyâ syntaxâ so much as something that just incidentally worked in that case because it was powerfully useful for something else. Rather, `something` will come from an argument or such, and will not be of type `Something`, but rather of type `&Something` or `&mut Something`, and so `for x in something { ⌠}` will automatically make x be of type `&Item` or `&mut Item`, as appropriate.
That is:
for x in something {
// What type is x? Depends on what type something is.
// ⢠something: Something â x: Item
// ⢠something: &Something â x: &Item
// ⢠something: &mut Something â x: &mut Item
}
(Mind you, Iâm not entirely disagreeing with the article here. The limits of IntoIteratorâs sugarness are annoying, and Iâm not convinced it was worth having in the language. Much of the rest of the article hinges upon wanting closures to be Ruby-style blocks (or procs or whatever they are, I canât remember) instead of closures; quite apart from introducing its own conceptual problems to balance those it solves, I donât believe it could coherently be implemented in a language with Rustâs constraints, especially with async.)The examples presented are not significant issues (in my opinion).
If someone from the Rust language development community is reading this, what I would really like to see is Rust supporting default arguments to functions. Coming from C++/Java/Python world, that is one language feature I sorely miss.
Is this person actually asking for Monads and Higher Kinded Types?
It would solve the problem they're raising but somehow I suspect that's now the solution they want to hear. And also the rust version would be quite hard to use due to the memory management model.
Both iterator examples have alternatives where you don't need a for loop. Try using for_each and you can collect an iterator of results into a single Result<_> you can then early return with.
Both of those are made cleaner by not using for loops.
The first example code doesnât look like anything you should ever do - use immutability and donât write rust like C.
The rest of the article is based on this use case, so I donât see the point.
Composability can be done in a functional style without mutating state. The outcome would likely be very different and easier with this approach if the time were taken to grok it.
Funny how the author both demands vague new ergonomics features (with no explanation how they might actually work), and at the same time demands the language team to stop adding new ergonomcis features.
> But the teams working on language design need to SLOW DOWN and focus on ergonomics and composability.
Except his concerns have already been explored.
> Rust experimented with all of these concepts at some point in its history, it wasnât out of ignorance that they were excluded.
https://without.boats/blog/the-problem-of-effects/
I think there are people still broadly exploring how to make it easier to write code that is generic over sync+async so that, for example, the standard library only needs one implementation of closures that can be either async or sync and work correctly. I canât recall if theyâre focusing on just async effects or also making it generic to Result. I canât find the docs page describing this effort for some reason but I swear I read something about it recently. The only other languages that have it are
* Go via Goroutines: doesnât fit Rustâs execution model of not having GC / controlling threading / no-std environments
* Zig: not explicit enough which gives up optimality for the sake of user convenience.
The Rust team is moving slowly and carefully here to make sure the solution they find balances ergonomics, complexity, and speed that people would expect of a âRustyâ solution.
The author is basically complaining âdrop all other work and focus on MY inconvenienceâ which is short sighted. Different people have different interests and capabilities. Itâs likely not helpful to throw more people at the problem given that itâs well known, studied, and people are indeed trying to tackle it.
Edit: found it by way of remembering that I came to it via the effing-mad crate which kind of gives you the composability albeit only working on nightly. Rust is calling this keyword generics https://blog.rust-lang.org/inside-rust/2022/07/27/keyword-ge.... If they pull it off, this will let you write generics over multiple effect systems transparently (failable, const, async, etc) for composability. I definitely want to see them try at the broader vision and deliver a high quality thing like they did with GATs but recognize this is a huge l language feature that will take time to get all the design and the fiddly implementation bits correct. Hopefully there will be short term features that can be delivered along the way to the full thing.
Rust's composibiliity inside a method / function doesn't really matter because it's easy to fix.
For me composibility problems with Rust come up with lifetimes, that's why I would like to give names to lifetimes of variables inside functions: as a step before extacting functions from it that isn't always possible.
Writing Rust recently got a bit easier because a certain large language model is able to work around all of these issues while also explaining why Rust designers made those choices.
I certainly hope that the language designers keep making the language easier to write for us totally fleshy humans that are not robots at all as well.
It sounds like the author is not aware of the `FromIterator` trait which is implemented for `Result` which allows short circuiting to a `Result` type.
Instead of collecting to:
Vec<Result<T,Error>>
You collect to: Result<Vec<T>,Error>
as your explicit type and your iterator will short circuit on the first error. I admit this was not obvious to me either and should be talked about more as it is very handy.The Kotlin compiler can handle chains of potentially().nullable()?.invocations(). Is the Rust early-error returning not a similar case, or am I missing something?
> Rust has a nice pretty syntax for iterating:
> `for x in &mut something {`
and a few paragraph later:
> So instead we need to use the ugly for-loop manual collection
> `for x in &iterator {`
So, is that syntax pretty or ugly?
> let res: Vec<_> = iterator.iter().map(|x| x.foo()?).collect();
This make sense to me? Why shouldn't it return early from the lambda in `map`? Had it return the "highest level function" in the scope then that would be bug prone imo. It is already possible to do fallible iteration:
> let res: Vec<_> = iterator.iter().map(|x| x.foo()).collect::<Vec<Result<_,_>>>()?;
However, I would like to mention that you should not try to force the syntax. This mantra is very misleading imo
> There should be one -- and preferably only one -- obvious way to do it.
Had Rust gone with your suggestion, imagine writing the equivalent of this?
``` fn foo() -> Result<String, String> {
Ok("hello".to_string())
}fn main() {
println!(
"{:?}",
(0..2)
.map(|_| {
foo()?;
foo()?;
foo()?;
foo()?;
foo()?;
Ok::<String,String>("world".to_string())
})
.collect::<Vec<Result<_, _>>>()
);
}
```How a "hacker news" website not supporting code snippet is beyond me.
""" for (i, x) in something.iter_mut().filter(|| {...}).enumerate() { x = (x) * i } """
uh, what's the alternative in other languages that's superior for what your trying to do here? Also I do not think this is ugly in any way.
The Rust apologists in the comments just don't get it. The fallow year is a great idea, particularly if you can somehow get Rust contributors to learn a Lisp and use it for that entire year before getting together and improving syntax matters.
Speaking of syntax:
for x in &mut something {
*x = (*x) * 2;
};
Not Rust user but would not it make sense for compiler to understand x instead of *x in this situation? I thought it is considered higher level language than C.> So instead we need to use the ugly for-loop manual collection
> let res = vec![]; > for x in &iterator { > res.push(x.foo()?); > }
I'm obviously not a rust programmer because I think that's much less ugly :-)
The main criticism expressed in this blog post is that in Rust, transforming a language construct into a seemingly equivalent one (typically using .map()) works in some cases (described as the "hello world" cases, which might be a bit exaggerated), but sometimes not (due to lifetime, fallibility, asyncâŚ).
For example, this code (real world example from two days ago):
fn main() {
let value = None;
let _processed_value = match value {
Some(value) => Some(process(value)),
None => None,
};
}
fn process(_value: u32) {}
can be transformed into this equivalent form: fn main() {
let value = None;
let _processed_value = value.map(|value| process(value));
}
fn process(_value: u32) {}
But in async Rust, this code works: #[tokio::main]
async fn main() {
let value = None;
let _processed_value = match value {
Some(value) => Some(process(value).await),
None => None,
};
}
async fn process(_value: u32) {}
But this one does not work: #[tokio::main]
async fn main() {
let value = None;
let _processed_value = value.map(|value| process(value).await);
}
async fn process(_value: u32) {}
It fails with the following error: error[E0728]: `await` is only allowed inside `async` functions and blocks
--> src/main.rs:4:64
|
4 | let _processed_value = value.map(|value| process(value).await);
| ------- ^^^^^^ only allowed inside `async` functions and blocks
| |
| this is not `async`
This is basically what is described as the sandwich problem: https://blog.rust-lang.org/inside-rust/2022/07/27/keyword-ge...I have to admit that I often need to refactor parts of code while I'm writing Rust code due to such issues, so I understand the author's point of view.
Rust feels like a very patchwork language. There's all sorts of issues (for loops, error handling, referencing/dereferencing, type inference, etc) that they've worked around by adding another very specific compiler behavior that completely falls apart in any other usage.
Like having to do `&mut **` to pass a reference to a function once you get out of the compiler's comfort zone.
Pythonistas when a language doesn't follow PEP8 and doesn't have a GIL.
>EXCEPT when you need to do anything else to the iterator, then its ugly
God forbid you use one (1) whole variable to write
let enumerated_something = something.iter_mut().filter(|| {...}).enumerate()
for (i, x) in enumerated_something { ... }
>BUT when you try to use it with iterators -- which are also amazing, I love using iterators -- IT DOESNT WORK.Pythonistas when you can't throw in the middle of a closure to end your loop. What is .map { } supposed to do ? Abort early but still stay alive ? Abort early and just return the first two elements that didn't fail ?
Or, you could define a Iterable<T>.map_or_none() that returns a bunch of Result/Options as an output, and be done with it.
>Again, the absolute lack of composability is astounding.
Coming from someone using python where extensions methods are a pipe dream and function(compositions(are(written(in(a(style(that(makes(me(want(to(go(back(to(lisp)))))))))))))), that's rich.
f-rust rated
One day Rust will be as hated as C++
Love this kind of articles, keep it up!
Small nitpicks presented as huge flaws by angry man
>I love Rust. I wish they would spend more time making it actually work for non hello-world use-cases.
Considering Rust has been used for anything, from basic userland utilities and databases, to high performance network stacks serving billions, compilers, and even Linux kernel drivers, I'd call this statement "not even wrong".
>What's the point of having the 'pretty' syntax if it only works in the simplest of cases?
That the "simplest of cases" are 90% of what you use 'for' for? The syntax should "make simple things easy, and hard things possible", and this is an example of exactly that!
>I hate syntax that only works in hello world examples
In which universe is basic iteration a "hello world" use case? It's used in basically EVERY program, all the time. That's "bread and butter", not "hello world" level.
And the rest also has a reason to be there, not that someone "infuriated" would try to go deeper.
>There should be one-- and preferably only one --obvious way to do it.
That's a design goal for another language. Which failed much worse than Rust at that, and for no good reason.