🦀🚀 Abstract over `Send` and `!Send` traits crates.io/crates/future_form
future_form EXTENSIONS.md
4.8 kB
Markdown

Extending FutureForm #

The FutureForm trait is open for extension. This document sketches out possible implementations 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), but allows any generic future wrapper.

Traced Futures (Hypothetical) #

A FutureForm that logs every poll — useful for debugging or instrumentation:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use futures::future::BoxFuture;

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.

Result-Wrapped Futures #

For operations that might fail at construction time:

use std::future::{Future, Ready, ready};
use std::pin::Pin;
use std::task::{Context, Poll};
use futures::future::BoxFuture;

/// Futures that capture a construction-time error.
pub struct Fallible;

pub enum FallibleFuture<'a, T> {
    Ok(BoxFuture<'a, T>),
    Err(Ready<T>), // For error cases with default/error value
}

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>;
}

Static Dispatch (Limited) #

True static dispatch without boxing is possible but limited. The future type must be nameable and generic:

use std::future::Ready;

/// For operations that complete immediately.
pub struct Immediate;

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

Usage:

impl MyTrait<Immediate> for MyType {
    fn get_value(&self) -> Ready<u32> {
        std::future::ready(42)
    }
}

This works for Ready, Pending, or any nameable future type, but not for async blocks (whose types are anonymous and unnameable).

Why Not Fully Unboxed? #

You might want:

impl FutureForm for Unboxed {
    type Future<'a, T: 'a> = impl Future<Output = T>; // Not valid
}

This doesn't work because:

  1. Associated types can't use impl Trait
  2. Async blocks have anonymous, unnameable types
  3. Each async block creates a different type, but FutureForm::Future must be a single type

The boxing in Sendable/Local is precisely what enables type erasure across different async implementations.

The FromFuture Trait #

The crate provides a FromFuture trait that abstracts over lifting a future into a kind's future type:

pub trait FromFuture<'a, T, F>
where
    T: 'a,
    F: Future<Output = T> + 'a,
{
    fn from_future(f: F) -> Self;
}

This is implemented for BoxFuture (requiring F: Send) and LocalBoxFuture (no Send requirement). Custom future types can implement this trait to enable uniform construction.

Usage is uniform across all forms via FutureForm::from_future:

impl Counter<Sendable> for Memory {
    fn next(&self) -> BoxFuture<'_, u32> {
        Sendable::from_future(async { self.val + 1 })
    }
}

impl Counter<Local> for Memory {
    fn next(&self) -> LocalBoxFuture<'_, u32> {
        Local::from_future(async { self.val + 1 })
    }
}

Using Custom Kinds with #[future_form] #

The #[future_form] macro supports custom FutureForm types alongside the built-in Sendable and Local:

#[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 replaces K::Future<'a, T> with CustomType::Future<'a, T> (the associated type) rather than a concrete type like BoxFuture.

Note: Custom types must implement FromFuture for the from_future pattern to work. Types like Immediate that use non-boxed futures (e.g., Ready<T>) require different construction patterns.