Rust syntax is only getting worse
Rust syntax is only getting worse
Posted Sep 24, 2025 16:12 UTC (Wed) by sturmflut (subscriber, #38256)Parent article: Canceling asynchronous Rust
Posted Sep 24, 2025 16:31 UTC (Wed)
by mb (subscriber, #50428)
[Link]
Rust doesn't force you to nest patterns like in the above code, though.
Posted Sep 24, 2025 16:56 UTC (Wed)
by geofft (subscriber, #59789)
[Link] (1 responses)
The real thing here is that it would be nice to give names to the two variants, maybe even just at one level, like
Maybe it'd be nice to have the ability to declare an alias for a type that has new names for the variants but is really just the same type, or something. I think there's also some work in progress to abstract the concept of Result into traits which might end up effectively doing this. That page also reminds me that there is an existing ControlFlow type that is effectively equivalent to Result but might be more suitable here (or might not, I'm not sure!).
These particular lines of code would also read better if it were matching on single level of encapsulation, something like
Posted Sep 27, 2025 18:10 UTC (Sat)
by NYKevin (subscriber, #129325)
[Link]
Re composition with Result: Honestly, I think this concern is exaggerated. Option and Result each provide conversion methods into the other, and it would not be unreasonably difficult to do the same with a custom enum. Once you have a conversion method, composition is straightforward enough.
As for general flexibility, this seems like a place where Rust's return-type dispatch might be useful. Here's how it might be spelled:
Clients can implement IntoTimeout if they want to return some exotic thing and convert it into a timeout result. Otherwise, the impls shown above should cover all reasonable cases.
Minor caveat: The real Timeout type is itself a Future, which timeout() returns directly. I did not want to bother actually showing how to implement a Future since it is irrelevant, so I repurposed the name Timeout for the final result type and hid the Future's name behind return-position impl trait. In a proper API, it is usually preferable to give a name to the type you return rather than hiding it in this way, at least in most cases.
Posted Sep 24, 2025 17:43 UTC (Wed)
by Altan (guest, #153331)
[Link]
Posted Sep 24, 2025 20:28 UTC (Wed)
by jbills (subscriber, #161176)
[Link]
Posted Sep 24, 2025 22:18 UTC (Wed)
by sunshowers (guest, #170655)
[Link]
I think the Ok(Ok(()) stuff is quite readable once you're used to it -- nested pattern matching is something to be used sparingly, but is reasonable to avoid added nesting levels (a match statement adds 2 nesting levels). It looks pretty reasonable with syntax highlighting.
Posted Sep 25, 2025 15:40 UTC (Thu)
by fishface60 (subscriber, #88700)
[Link]
It's an abstraction designed to give you the option of deciding "actually, any timeout is an error and I want to bail out immediately" using the ? operator.
A possible alternative formulation would be
Ok(permit) => ...
by the timeout function extending the Error value of the Result with a new variant, though since TimeoutError would be defined like this
enum TimeoutError<T, E> {
and that's logically equivalent to Result there's benefits to reusing it instead of defining an extra type
timeout() could define its own return type as
enum TimeoutResult<T, E> {
and then you could match on it more simply as
TimeoutResult::Ok(permit) => ...
but in both cases you're adding extra cognitive overhead for new types and losing the helper functions that are part of Result,
Ok(Ok(...)) matching isn't an inherent problem in rust, it's a problem that developer time is finite and adding a new type to express the result of a timeout costs more than just wrapping your Results.
Rust syntax is only getting worse
Many languages support this.
But it's pretty common, because it actually is quite readable, once you understand pattern matching.
It's not really worse than, say, a double pointer in C, where the outer pointer can be NULL or point to some inner pointer, which itself can be NULL. You have to check for both of those, and they can mean different things. (Though you can counter that defending syntax by saying "what about double pointers in C" is not much of a defense.)
Rust syntax is only getting worse
loop {
match timeout(Duration::from_secs(5), rx.recv()).await {
Happened(Ok(msg)) => process(msg),
Happened(Err(_)) => return,
Timeout(_) => println!("No messages for 5 seconds"),
}
}
but the tradeoff is that there's a good bit of functionality that works with the pre-existing Result type (Ok/Error) that people are used to using, and coming up with a new type for this purpose loses all of that.
loop {
match timeout(Duration::from_secs(5), rx.recv()).await {
Ok(msg) => process(msg),
Err(_) => return,
Timeout(_) => println!("No messages for 5 seconds"),
}
}
but now you're inventing a brand new type with three variants that has none of the existing behavior of the existing types (e.g., how do you compose it with a normal Result? what happens if you use the question-mark syntax on it?), and you're also constraining that second argument to timeout to be an async function that returns a Result, so you have those two variants. The advantage of the current design is that timeout wraps an async function that returns anything. Losing that flexibility would make async Rust code both harder to read and harder to write.
Rust syntax is only getting worse
use std::convert::Infallible; // TODO: Replace with ! when it is stabilized in some future version of Rust.
use std::time::Duration;
use tokio::time::error::Elapsed;
pub enum Timeout<V, E>{
Ok(V),
Err(E),
Timeout(Elapsed),
}
// const wrap needed so that Result<V, Infallible> and V don't
// have overlapping impls. Ugly hack, but it should work.
pub trait IntoTimeout<V, E, const wrap: bool>{
fn into_timeout(self) -> Timeout<V, E>;
}
impl<V, E> IntoTimeout<V, E, false> for Result<V, E>{
fn into_timeout(self) -> Timeout<V, E>{
match self {
Ok(v) => Timeout::Ok(v),
Err(e) => Timeout::Err(e),
}
}
}
impl<T> IntoTimeout<T, (), false> for Option<T>{
fn into_timeout(self) -> Timeout<T, ()>{
match self {
Some(t) => Timeout::Ok(t),
None => Timeout::Err(()),
}
}
}
impl<T> IntoTimeout<T, Infallible, true> for T{
fn into_timeout(self) -> Timeout<T, Infallible>{
Timeout::Ok(self)
}
}
pub fn timeout<F, T, V, E, const wrap: bool>(d: Duration, fut: F) -> impl Future<Output=Timeout<V, E>>
where
F: IntoFuture<Output=T>,
T: IntoTimeout<V, E, wrap>
{
// Ugly hack to work around ! not implementing Future.
// But this is just placeholder code anyway.
std::future::ready(todo!())
}
Rust syntax is only getting worse
Rust syntax is only getting worse
Rust syntax is only getting worse
Rust syntax is only getting worse
Err(TimeoutError::Timeout(...)) => ...
Err(TimeoutError::Other(...)) => ...
Timeout(T),
Other(E),
}
Timeout(TimeoutError),
Ok(T),
Err(E),
}
TimeoutResult::Timeout(...)) => ...
TimeoutResult::Err(...)) => ...
and you have to narrow the set of operations you call timeout on to things that return a Result.
