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.For a good while my X's were a nightly only experimental API, but slowly and surely they make it into stable. (Hash|Btree)Map's `.entry` methods (and friends) were another example.
Generally speaking these methods are super helpful and someone else already thought of it. From my experience at least.
.. also this article was infuriating hah.
let res = iterator.iter().map(|x| x.foo()).collect::<Result<Vec<_>, _>>()?;Kind of like how the answer is always ‘traverse’ in Scala.
.collect() gets overloaded a lot to deal with the intricacies of the return type, which usually says a lot about the computation, e.g. Result or HashSet.
`let res: Result<Vec<_>, _> = iterator.iter().map(|x| x.foo()).collect();`
Left to right reading this statement is "iterate over the values and map it to the output of foo for each value. Then collect that map into a vector."
His other example
``` for (i, x) in something.iter_mut().filter(|| {...}).enumerate() { x = (x) * i } ```
can also be read from left to right as "iterate over something, then filter and enumerate the values"
It's much more explanatory than top down structures with 10 lines to explain the same thing.
Using the question mark in a closure in general is confusing.
Doing ? on the return value of a statement doesn't make sense. Normally ? is used for: here is this Result (I forgot the trait's name), and give me the Ok(), and if it is Err() then RETURN to the caller.
So the function itself must be of type Result<_, _>.
With the lambda in the example it is unclear by reading the code what the ? should do. Stop at the first error and return Err? Or should the return value be Result<Vec<_>, _>?
One can use the try_ functions on an iterator.
Consider:
for item in iterable.iter() {
item.foo()?;
}
You will exit the loop as soon as there is an error. But with .map().collect(), you iterate over the whole list, and then exit if one of the items yield an error.EDIT: Thank you for your answers, it seems I was wrong and it does short-cricuit!
let v: Option<Vec<i32>> = (0..5).map(|i| {
print!("{i} ");
if i == 3 { None } else { Some(i) }
}).collect();
// prints 0 1 2 3Why 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.
Don't project your reaction to unfamiliarity as a general law. Instead, either ignore the languages with syntaxes you don't like, or just go and learn them before forming an opinion.
And ffs don't talk about syntax being "pretty" or "ugly". That's the epitome of subjectivity, I have a hard time trying to understand why would anyone out of their teens want to argue about that. Ancient Romans knew that de gustibus non disputandum est, yet here we are, "discussing" what is beautiful and what's not.
[1] There are some constraints that stem from the optics and mechanics of an eyeball. Other than that, it's purely subjective.
I think that style is a matter of taste, and some part of syntax can be regarded as styling decisions, like with placement of tabs vs spaces, braces alignment, snake_case vs camelCase etc.
But there is a continuum of syntax readability, brainfuck is less easy to read than Perl that is still easier to read than Sed.
And I think exactly the opposite about familiarity, the less familiar you are with a syntax the better your judgment is about its readability.
Of course that can be learned, but that is not the point.
Programming languages should really strive to be as easy to read as possible, even at the cost of being more difficult to write.
Code should never look arcane, especially to someone unfamiliar with the language / codebase.
CommonLisp can do this without that syntax
Anonymous closure arguments, aka $0, are probably the ugliest part of its syntax.
numbers.contains(where: { $0 > 0 })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.
Anything that can't be done gets exactly 0% of the usage. I don't know where you got 90% from.
The author clearly comes from a mindset of trying to write Haskell code in Rust. But the criticism is fair, those are 3 very ugly aspects of the language, and it may be possible to improve them without breaking the general behavior... And even if it isn't, it is well worth it to be aware of them.
I'm not sure what the first sentence means.
Plain iteration across all elements is the most common case for "for".
That's where I get the 90% from (meaning, most of the time you'll be doing that).
for/in without filters etc., is not something that's just for "hello world" style programs (only useful as a very crude example that you wont find in any real program) - as the author describes it.
It's the very opposite: what you will most commony use in every program you write multiple times.
for x in &mut something {
*x = (*x) * 2;
}
Here (above) it looks like the `&mut` is allowing the iterator to be mutable (https://doc.rust-lang.org/std/keyword.mut.html). for (i, x) in something.iter_mut().filter(|| {...}).enumerate() {
*x = (*x) * i
}
The `mut` keyword isn't needed here because, I assume if the above assumption is correct, "iter_mut()" is returning a mutable iterator. Is there some reason the result of "something.iter_mut().filter(|| {...}).enumerate()" can't be saved to a variable and then used as the "something" in the first example?Anyway, this post reminds me of an article, "Why Programming Language X is Unambiguously Better Than Programming Language Y"[0]. Mostly talking about how [poorly written Y code] is why Y is a bad language and [beautiful and elegant X code] is why X is a good language.
[0] https://news.ycombinator.com/item?id=6960398; unfortunately the link is dead and I can't find it on the domain
You absolutely can do something like `let iter = something.iter_mut().filter(|| {...}).enumerate();` In Rust, Iterators are just need to implement a Trait (kindof like an Go or Java interface) so they can be put in normal variables just fine.
> The `mut` keyword isn't needed here because, I assume if the above assumption is correct, "iter_mut()" is returning a mutable iterator.
Every for loop implicitly calls `.into_iter()` on the thing to loop over; the docs are decent at explaining what IntoIter is [0]. By doing `&mut something` instead of just `something` you get a different IntoIter implementation which has very different semantics. Typically `(&mut something).into_iter()` would have the same behavior and might even result in the same type as `something.iter_mut()`.
[0]: https://doc.rust-lang.org/std/iter/trait.IntoIterator.html
Here is an example to illustrate what I mean (i did not test to see if it compiles so there may be some mistakes):
struct Foo<T>(Vec<T>);
impl IntoIterator for Foo<T> {
type IntoIter = std::vec::IntoIter<T>;
type Item = T;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a Foo<T> {
type IntoIter = std::slice::Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.0.as_slice().iter()
}
}
impl<'a> IntoIterator for &'a mut Foo<T> {
type IntoIter = std::slice::IterMut<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.0.as_slice().iter_mut()
}
}
Doing "for x in T" calls the first method, "for x in &T" calls the second, and "for x in &mut T" calls the last one.As you see here, each IntoIterator returns a completely different type, and the first IntoIterator actually consumes the struct Foo as well!
There are also the methods "iter" and "iter_mut", these are just there for convention and technically don't need to exist for the iterator related traits. It's useful for doing iterator pipelines that don't have any for loop.
The author really likes Rust. He's not some naysayer. He'd just like Rust to be even better. He provides constructive feedback.
The author intents it to be taken literally. He even goes on to expand on the "literalliness" of his put-downs, with several paragraphs of invalid arguments furthering the exaxt same non-point.
>The author really likes Rust.
Liking and understanding are too different things.
>He provides constructive feedback.
See, this is where I beg to differ.
...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...
If you don't like Rust, don't use it. No one is forcing anyone. In particular, languages such as C are incredibly slow moving and well understood.
Rust's goal seems to be replacing C++, which kind of means people will have to deal with it. If it's going to be in all these domains, people have to be willing to take criticism constructively.
Saying "you don't have to use all these features" doesn't work either - just look at C++. It's the mess that it is precisely because the language designers never learned to say no to the next cool thing someone wanted to add in.
At the moment, Rust is opinionated and polarizing. Some people who really like it try to push it everywhere. If and as that happens, people who disagree with a lot of the design choices Rust is making are going to have to have a voice in the community.
To be immediately followed by
>Marginal systems language (D/Nim/V/Go/Odin/Vale) is useless bikeshedding and has no corporate backer, why would anyone waste their time with this, they should use an established well-known language.
PL debates are whiplash.
Those are the demands of passionate early adopters.
There are *a lot* of people who don’t like things that move that quickly.
Are the rust teams sure they’re serving their user base? Or are they actually pandering to a vocal minority?
Are they actually addressing real pain points? Or just working on cool stuff?
Have they even asked?
Actually, there is a lot of "force" and "forcing" behind Rust.
And closures in Rust can do things that "fully named functions" cannot. Let that one sink in.
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.Like (pseudocode)
var i = 0
for(item in items) {
item.DoThing(i)
i+=1
}
Does rust not allow that kind of mutation and access of a variable from outside the loop within the loop?https://play.rust-lang.org/?version=stable&mode=debug&editio...
struct Item(u32);
impl Item {
fn do_thing(&self, i: usize) {
println!("The value of `i` is: {i}");
}
}
fn main() {
let items = vec![Item(1), Item(2), Item(3)];
for (i, item) in items.iter().enumerate() {
item.do_thing(i);
}
}I think what they were pointing out (and I agree with) is that the mixing of combinators and imperative-style for loops that's going on in the post doesn't feel very natural. Typically if you would _either_ do
blah
.into_iter()
.filter(|x| x.some_condition())
.for_each(|x| x.do_something());
or for x in blah {
if !x.some_condition() {
continue;
}
x.do_something();
}
But `for x in blah.into_iter().filter(...` is a bit unusual. let mut i = 0;
for item in items {
item.method(i);
i += 1;
}
I don't know why you'd it that way, but Rust definitely isn't stopping you from doing that.e: and as running example: https://play.rust-lang.org/?version=stable&mode=debug&editio...
Compare all the crazy things people build out of channels in go, or a similar tendency in python to over-do the generator expressions.
lol 3 people said the same thing at the same time. rust strike force assemble
> 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();No. As a user for just short of a decade, I have probably used .for_each() less than a dozen times in total (grep over the current state of most of the code I’ve ever written: three matches). I know it’s there, but it doesn’t often feel right, and I have certainly regularly used for loops where closure capture rules mean that I couldn’t have used .for_each(), which is strictly less expressive. Perhaps if I had come from a language like Ruby I might use it more, but I came more from Python and JavaScript.
I don’t think the article showed a good formatting, however; I’d write it more like this:
for (i, x) in something
.iter_mut()
.filter(|| {...})
.enumerate()
{
*x = (*x) * i;
}I think this is fine. I think Rust's strength is as a multi-paradigm language. I would say for this specific code, a for_each() makes more sense than the alternatives (on the margin), but for more complex use cases, where something like a try_for_each() might be used, where you may need to return 3 different error types, which for some reason one type can't be easily coerced out of the closure, a for loop makes tons of sense. But I think that's a 5% use case, and in the other 95% of cases the iterator approach is to be preferred. For many sound technical reasons, as well as for more squishy code readability reasons (maps and folds and filters are simply more readable to my eye).
Again I strictly don't have a problem with anything in your comment, or anyone using for loops, but if I was writing your for loop and my concern was the author's re: pretty syntax, I'd write it as:
let y_iter = some.iter().built().up().like().this();
for x in y_iter {
...
}I think of the iterator building as mapping values from an iterator to values to consume - the for loop consumes them. for_each with side-effects just confuses me later in a lot of those cases - it has it's place but so do for loops. (also, closure issues around variable capture, moves, etc come into play with for_each).
If this is your concern, and you really must write it like that, I'd break it up, into your iter and your loop.
let y_iter = some.iter().built().up().like().this();
for x in y_iter {
...
}No, because that doesn't evaluate the iterator.
let it = something
.iter_mut()
.filter(|item| ...)
.enumerate();
for (i, x) in it {
...
}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
This isn't true, and it's a damn shame that it is so close to being true. The biggest feature I miss from Common Lisp is that closures capture blocks and tags, and this is but the simplest form of cross-function control flow available in PL design. Algebraic effects are the next big thing.
In ruby returning from a proc will return from where the proc was defined, usually resulting in a bug
optionList.map { option ->
val result = option.getOrElse {
Log.d("Well that's fucked up")
return@map null
}
...more logic
}
and further in, result will not only have early returned, but will also have the proper, unwrapped type. fn foo(inputs: &[Input]) -> Result<Vec<Output>, Error> {
inputs
.iter()
.map(|input| { input.something_fallible()? })
.collect()
}
that ? is an early return from the closure, not foo. The correct way to do this is: fn foo(inputs: &[Input]) -> Result<Vec<Output>, Error> {
inputs
.iter()
.map(|input| { input.something_fallible() })
.collect::<Result<Vec<Output>, Error>>()
}
ref: https://play.rust-lang.org/?version=stable&mode=debug&editio... // never do this, it’s rude
let x = (|| potentially().nullable()?.invocations())();
Or with Option::map, and_then and friends: // this is the way
let x = potentially().nullable().map(|y| y.invocations());Both of those are made cleaner by not using for loops.
If you're going to do .filter , presumably you can do a .map or something and just supply a small lambda which does what was in the body of the for loop in the example?
Alternatively you could just do the filter part using an if statement in the for loop. Either of those syntaxes would be much more straightforward than doing half of one and half of the other. It just doesn't make sense to do that and point at the result as being unergonomic.
Secondly the fact that for and for_each have different characteristics is as someone else has pointed out fundamentally just that blocks and functions are different things. ie it breaks the author's mental model because their mental model is just wrong.
The ? operator can be used both inside and after it like so:
iter.try_for_each(|_| { … xxx?; … })?;https://blog.rust-lang.org/inside-rust/2022/07/27/keyword-ge...
If your for loop body is infallible then for_each is good enough.
If you're trying to 'collect' some iterator into some data structure T, you can collect into Result<T, _> and question mark on that.
Like having to do `&mut **` to pass a reference to a function once you get out of the compiler's comfort zone.
In contrast, academic languages try to identify broadly-applicable abstract primitives and expose those, allowing users to implement the behaviors they need in libraries. The result feels more consistent at a language level because you can tie language features very closely to an underlying theory of computation.
I've written a lot of C++ (industrial) and Haskell (academic). Although I love how Haskell allows users to (for example) define their own control-flow statements, that "bag of primitives" nature leads to Haskell projects easily forming their own dialect of the language[0] without even having to resort to macros.
[0] This is also a common criticism of Lisp and Forth.
In my mind an industrial language needs to be boring and obvious. For instance, Go is very boring, and it actively discourages by it's design "clever" programming. Java Go and C are industrial languages. C++ is far too clever these days.
All that said rust is far better than Java and Go for interfacing with low level systems, and of course the safety improvements from the borrow checker are very valuable, despite the frustrations it often causes.
I don't dislike rust, but it sure could use a lot of polish in my opinion, making obvious things easy and discouraging clever things.
I'm curious about your use case for this. `&mut *` I've seen, for instance to dereference a RwLockWriteGuard, but why the double dereference?
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.
It’s not much different from higher-order functions in JavaScript, really. (Other than the fact that JavaScript has exceptions.)
let-else and GAT were introduced in 1.65 (November 2022).
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.
In Ruby, iteration can be performed by passing a sort-of-closure called a block. Unlike passing a closure (function), returning early from a block acts like returning from a for loop.
If a language has a garbage collector and no user-visible separation between stack and heap allocation, then there's lots of interesting things that language can do in terms of building "fluent" APIs.
>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.
I think you got that backwards. I'm pretty sure it's supposed to be lisp(to(back(go(to(want(me(makes(that(style(a(in(written(are(compositions(function))))))))))))))
(-> function compositions are written in a style that makes me want to go back to lisp)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.
Indent your code by two spaces. 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.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.
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.
In principle you could have a variant scoped function syntax that does that, but that will complicate both the language implementation and syntax
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.)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.
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.
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.
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.
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.
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.> `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?
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.
> 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 :-)
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.