|
|
Subscribe / Log in / New account

Rust syntax is only getting worse

Rust syntax is only getting worse

Posted Sep 24, 2025 16:56 UTC (Wed) by geofft (subscriber, #59789)
In reply to: Rust syntax is only getting worse by sturmflut
Parent article: Canceling asynchronous Rust

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.


to post comments

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.


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