๐Ÿฆ€๐Ÿš€ Abstract over Send and !Send traits crates.io/crates/future_form
0

Configure Feed

Select the types of activity you want to include in your feed.

future_form / design / extensibility.md
4.3 kB

Extensibility#

The FutureForm trait is open for extension. You can implement custom forms beyond the built-in Sendable and Local.

The Constraint#

pub trait FutureForm {
    type Future<'a, T: 'a>: Future<Output = T> + 'a;
}

Any implementation must provide a Future type that:

  1. Works for any output type T
  2. Works for any lifetime 'a
  3. Implements Future<Output = T>

This rules out concrete async block types (which have fixed output types and anonymous types), but allows any generic future wrapper.

Custom Forms#

Traced Futures#

A FutureForm that logs every poll โ€” useful for debugging or instrumentation:

pub struct Traced;

pub struct TracedFuture<'a, T> {
    inner: BoxFuture<'a, T>,
    name: &'static str,
}

impl<'a, T> Future for TracedFuture<'a, T> {
    type Output = T;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
        let this = self.as_mut().get_mut();
        eprintln!("[trace] polling {}", this.name);
        Pin::new(&mut this.inner).poll(cx)
    }
}

impl FutureForm for Traced {
    type Future<'a, T: 'a> = TracedFuture<'a, T>;
}

This pattern extends to metrics collection, cancellation tokens, or any wrapper that needs to observe or modify future behavior.

Static Dispatch (Limited)#

True static dispatch without boxing is possible when the future type is nameable:

pub struct Immediate;

impl FutureForm for Immediate {
    type Future<'a, T: 'a> = Ready<T>;
}

This works for Ready, Pending, or any other nameable future type. It does not work for async blocks, whose types are anonymous and unnameable. This is the fundamental reason boxing exists in the first place โ€” see Boxing Costs and TAIT Migration.

Result-Wrapped Futures#

For operations that might fail at construction time:

pub struct Fallible;

pub enum FallibleFuture<'a, T> {
    Ok(BoxFuture<'a, T>),
    Err(Ready<T>),
}

impl<'a, T> Future for FallibleFuture<'a, T> {
    type Output = T;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
        match self.get_mut() {
            Self::Ok(f) => Pin::new(f).poll(cx),
            Self::Err(f) => Pin::new(f).poll(cx),
        }
    }
}

impl FutureForm for Fallible {
    type Future<'a, T: 'a> = FallibleFuture<'a, T>;
}

FromFuture and FromReady#

Custom future types should implement FromFuture and FromReady to enable the K::from_future(async { ... }) and K::ready(value) construction patterns:

impl<'a, T, F> FromFuture<'a, T, F> for TracedFuture<'a, T>
where
    T: 'a,
    F: Future<Output = T> + Send + 'a,
{
    fn from_future(f: F) -> Self {
        TracedFuture {
            inner: Box::pin(f),
            name: "unknown",
        }
    }
}

Without these impls, the custom form cannot be used with K::from_future() โ€” the trait bound Self::Future<'a, T>: FromFuture<'a, T, F> on FutureForm::from_future will fail.

NOTE

Types like Immediate that use non-boxed futures (e.g., Ready<T>) require different construction patterns. FromFuture expects a future as input, but Ready<T> is constructed from a value. Use FromReady for the value-to-future case.

Using Custom Forms with the Macro#

The #[future_form] macro supports custom types alongside built-ins:

#[future_form(Sendable, Local, Traced)]
impl<K: FutureForm> MyTrait<K> for MyType<K> {
    fn operation(&self) -> K::Future<'_, u32> {
        K::from_future(async { self.value })
    }
}

For custom types, the macro rewrites K::Future<'a, T> to CustomType::Future<'a, T> (the associated type) rather than a concrete path like BoxFuture. This means the generated code references the GAT, and the actual future type resolves through the FutureForm impl.

Why Not a Concrete Path?#

Built-in variants (Sendable, Local) get special treatment: the macro knows their concrete future types and rewrites directly to BoxFuture, LocalBoxFuture, etc. This produces cleaner generated code and better error messages.

Custom types could be anything, so the macro falls back to the associated type path. This is correct but means error messages may be less specific when type mismatches occur.