yoshuawuyts / futures-concurrency

Structured concurrency operations for async Rust
https://docs.rs/futures-concurrency
Apache License 2.0
382 stars 29 forks source link

Handling control flow #158

Closed bradleyharden closed 9 months ago

bradleyharden commented 9 months ago

Hi,

I'm in the process of converting a small project of mine from tokio to smol. In the process, I'm also removing instances of tokio::select! and replacing them with constructs from futures-concurrency. For the most part, the changes have been simple. That's great, because I would prefer to avoid macros if they don't significantly simplify the code.

However, I think I found one instance where the macro wins out. I wanted to bring it up here, since I don't see any existing issues discussing it.

I had loop that would await either more data or a signal. If I got a signal, I would break the loop.

loop {
    let data = tokio::select! {
        data = future() => data,
        _ = signal.recv() => break,
    };
}

Implementing this with Race is a bit tricky. You need to somehow unify the types. I chose ControlFlow, as it seemed the most natural. Here's what I came up with:

loop {
    let data = async {
        let data = future().await;
        ControlFlow::Continue(data)
    };
    let sig = async {
        let _ = signal.next().await;
        ControlFlow::Break(())
    }
    let flow = (data, sig).race().await;
    let ControlFlow::Continue(data) = flow else {
        break;
    };
}

Can you think of any better way to implement this? I think this is one area where the macro has a natural advantage. Is there any simpler way to do this in the smol ecosystem?

yoshuawuyts commented 9 months ago

Hi! That's a good question. I'd recommend you consider using Stream::merge instead:

enum Message<T> {
    Data(T),
    End,
}

let data = stream::once(future.map(Message::Data));
let end = signal.map(|_| Message::End);

let mut s = (data, end).merge();
while let Some(msg) = s.next().await {
    match msg {
        Data(data) => ..,
        End => break,
    }
}

I describe how this works in more detail here.

bradleyharden commented 9 months ago

Ah, that's a good idea. I didn't think to use the fact that Signals already implements Stream. Using .map() certainly helps cut down on the boilerplate too.

Thanks for the tip. I think this is clean enough for me to justify moving away from a macro.