futures-rs icon indicating copy to clipboard operation
futures-rs copied to clipboard

select! and Option<Future<…>>

Open valpackett opened this issue 4 years ago • 3 comments

A common situation in my current project is that I need to select! between the results of calling async methods on various things that might not exist. Unfortunately the select macro doesn't seem to have any native support for conditionally including branches in the select, something that would be like

futures::select! {
  if let Some(ref mut obj) = maybe_existing_object {
    event = obj.async_method().fuse() => do_something_with(event).await
  },
  event = always_existing_object.async_method().fuse() => do_something_with(event).await,
}

And it's not possible to do maybe_obj.map(|o| o.async_method().fuse()).unwrap_or_else(|| future::Fuse::terminated()) (or future::pending() etc.) because, well, async methods — that never-resolving future we use for when the object doesn't exist doesn't match the impl Future opaque type of the async function, so they're incompatible unless we go with the boxing and dyn route and all that inefficiency and sadness.

So far this is the best thing I could come up with:

pub struct MaybeFuture<F>(Option<F>);

impl<F> MaybeFuture<F> {
    pub fn new(f: Option<F>) -> MaybeFuture<F> {
        MaybeFuture(f)
    }
}

impl<F: Future + Unpin> Future for MaybeFuture<F> {
    type Output = F::Output;

    fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
        // XXX: unchecked should be fine here, is it faster than Unpin?
        if let Some(ref mut f) = self.get_mut().0 {
            Future::poll(Pin::new(f), cx)
        } else {
            task::Poll::Pending
        }
    }
}

impl<F: FusedFuture + Unpin> FusedFuture for MaybeFuture<F> {
    fn is_terminated(&self) -> bool {
        if let Some(ref f) = self.0 {
            f.is_terminated()
        } else {
            true
        }
    }
}
        futures::select! {
            ev = this.keyboard_events.select_next_some() => this.on_keyboard_event(ev).await,
            ev = MaybeFuture::new(this.ptr.as_mut().map(|p| p.next())) => this.on_pointer_event(ev).await,
            ev = MaybeFuture::new(this.touch.as_mut().map(|p| p.next())) => this.on_touch_event(ev).await,
            // ......
        }

Maybe there should be direct impl<F: Future> Future for Option<F> like this? Or native syntax for conditionally including select! branches depending on Options being Some or None (but that might be more difficult)?

valpackett avatar Nov 25 '20 16:11 valpackett

I think the only correct solution here is to support fallible patterns, as tokio supports. (currently, infallible patterns are only accepted)

        // XXX: unchecked should be fine here, is it faster than Unpin?

No, Pin::unchecked_* has no effect on performance. Unpin is a compile time checker, does nothing at runtime.

            task::Poll::Pending

Note that this is not preferred in many case. see #1765 for more.

taiki-e avatar Nov 26 '20 05:11 taiki-e

"fallible patterns" (Some(x) = some.future() =>, right?) sounds like something that happens with the result of the future, not the future itself?

Though looks like tokio has if preconditions that could be used like x = maybe_future.unwrap(), if maybe_future.is_some() => {......} — better than nothing, but would be nicer without unwraps.

valpackett avatar Nov 26 '20 17:11 valpackett

sounds like something that happens with the result of the future

Yeah, it probably works well when combined with OptionFuture.

taiki-e avatar Nov 27 '20 14:11 taiki-e