|
|
Subscribe / Log in / New account

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

Ok(Ok(permit)) and Ok(Err(_)) have got to be some of the worst notations I've ever seen. That's write-only code.


to post comments

Rust syntax is only getting worse

Posted Sep 24, 2025 16:31 UTC (Wed) by mb (subscriber, #50428) [Link]

Well, it's pattern matching.
Many languages support this.

Rust doesn't force you to nest patterns like in the above code, though.
But it's pretty common, because it actually is quite readable, once you understand pattern matching.

Rust syntax is only getting worse

Posted Sep 24, 2025 16:56 UTC (Wed) by geofft (subscriber, #59789) [Link] (1 responses)

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.)

The real thing here is that it would be nice to give names to the two variants, maybe even just at one level, like

    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.

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

    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

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:

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!())
}

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.

Rust syntax is only getting worse

Posted Sep 24, 2025 17:43 UTC (Wed) by Altan (guest, #153331) [Link]

Pattern matching is not something new for programming languages, there are better usages of it in other languages but Rust also has nice syntax and support for it.

Rust syntax is only getting worse

Posted Sep 24, 2025 20:28 UTC (Wed) by jbills (subscriber, #161176) [Link]

If it is getting worse (I disagree), this syntax isn't an example. This syntax (pattern matching) has been in the language for a very long time. Regardless, it is entirely transparent if you have written enough Rust to know how the data types work. You have two functions with two independent sources of errors that are getting nested. You need to unwrap them independently to be able to handle them independently.

Rust syntax is only getting worse

Posted Sep 24, 2025 22:18 UTC (Wed) by sunshowers (guest, #170655) [Link]

(I presented the talk -- thanks for featuring it on LWN!)

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.

Rust syntax is only getting worse

Posted Sep 25, 2025 15:40 UTC (Thu) by fishface60 (subscriber, #88700) [Link]

> Ok(Ok(permit)) and Ok(Err(_)) have got to be some of the worst notations I've ever seen

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) => ...
Err(TimeoutError::Timeout(...)) => ...
Err(TimeoutError::Other(...)) => ...

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> {
Timeout(T),
Other(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> {
Timeout(TimeoutError),
Ok(T),
Err(E),
}

and then you could match on it more simply as

TimeoutResult::Ok(permit) => ...
TimeoutResult::Timeout(...)) => ...
TimeoutResult::Err(...)) => ...

but in both cases you're adding extra cognitive overhead for new types and losing the helper functions that are part of Result,
and you have to narrow the set of operations you call timeout on to things that return a 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.


Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds