๐Ÿฆ€ Functional Rust

322: The Future Trait and Poll

Difficulty: 4 Level: Expert A Future is just one method: `poll`. The runtime calls it. If the work is done, return `Ready`. If not, return `Pending` and arrange to be woken later.

The Problem This Solves

When you write `async fn` and `.await`, the compiler generates state machine code for you. But what is a Future, actually? Understanding the `Future` trait answers this โ€” and it matters when you need to implement your own async primitive, integrate non-async code into an async runtime, or debug why a future is never waking up. Without understanding `poll`, async code feels like magic. You don't know why `await` suspends, how the runtime knows when to resume, or what the `Waker` is for. This leads to subtle bugs: futures that never wake, busy-polling that wastes CPU, or deadlocks from holding locks across `.await`. Implementing `Future` manually also reveals that the entire async machinery is surprisingly simple โ€” one method, two return values, one callback mechanism.

The Intuition

Imagine a restaurant. You order food (create a future). The waiter doesn't stand next to the kitchen watching โ€” they go serve other tables. The kitchen calls the waiter when the order is ready (the `Waker`). The waiter comes back and delivers (returns `Poll::Ready`). The `poll` method is: "Is the food ready?" The answer is either `Ready(food)` or `Pending` (with a promise to call you when it is). The runtime keeps a list of pending tasks and polls them when they signal readiness. In Python, `asyncio` hides this behind coroutines. In JavaScript, Promises chain callbacks. Rust exposes the mechanism directly โ€” which gives you full control and zero runtime overhead.
poll() โ†’ Poll::Ready(value)   โ† work done, here's the result
poll() โ†’ Poll::Pending        โ† not done yet, we'll wake you when ready

How It Works in Rust

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

struct DelayedValue { value: i32, remaining: u32 }

impl Future for DelayedValue {
 type Output = i32;

 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
     if self.remaining == 0 {
         Poll::Ready(self.value)         // work is done
     } else {
         self.remaining -= 1;
         cx.waker().wake_by_ref();       // tell the runtime: try again soon
         Poll::Pending                   // not ready yet
     }
 }
}
`Pin<&mut Self>` prevents the future from being moved in memory while it's being polled โ€” important for self-referential state machines that the compiler generates from `async fn`. `cx.waker().wake_by_ref()` is how you tell the runtime "I'll be ready soon, poll me again." In real I/O, the OS (via epoll/kqueue) calls the waker when a socket becomes readable. The `block_on` in this example is a minimal hand-rolled executor โ€” it just loops calling `poll` until `Ready`. Real runtimes (tokio, async-std) are far more sophisticated but implement the same interface.

What This Unlocks

Key Differences

ConceptOCamlRust
Core async abstraction`Lwt.t` (promise/thread)`Future` trait with `poll`
State transitionimplicit in Lwt machineryexplicit `Poll::Ready` / `Poll::Pending`
Wakeup mechanismcallback registered on promise`Waker::wake()` via `Context`
Pinningnot needed`Pin<&mut Self>` prevents moves
ExecutorLwt main loopany runtime that calls `poll`
//! # The Future Trait and Poll
//!
//! Understanding the core Future trait: `poll`, `Poll::Ready`, `Poll::Pending`,
//! and how to implement custom futures manually.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

/// A future that returns a value after being polled a certain number of times.
/// Demonstrates the manual implementation of the Future trait.
pub struct DelayedValue {
    value: i32,
    remaining_polls: u32,
}

impl DelayedValue {
    /// Create a new delayed value that will be ready after `polls` poll calls.
    pub fn new(value: i32, polls: u32) -> Self {
        Self {
            value,
            remaining_polls: polls,
        }
    }
}

impl Future for DelayedValue {
    type Output = i32;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.remaining_polls == 0 {
            Poll::Ready(self.value)
        } else {
            self.remaining_polls -= 1;
            // Schedule a wakeup so the runtime knows to poll again
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

/// A future that is immediately ready with a value.
pub struct Ready<T> {
    value: Option<T>,
}

impl<T> Ready<T> {
    pub fn new(value: T) -> Self {
        Self { value: Some(value) }
    }
}

impl<T: Unpin> Future for Ready<T> {
    type Output = T;

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        match self.get_mut().value.take() {
            Some(v) => Poll::Ready(v),
            None => panic!("Ready polled after completion"),
        }
    }
}

/// A future that counts how many times it was polled before returning.
pub struct PollCounter {
    target: u32,
    current: u32,
}

impl PollCounter {
    pub fn new(target: u32) -> Self {
        Self { target, current: 0 }
    }
}

impl Future for PollCounter {
    type Output = u32;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.current += 1;
        if self.current >= self.target {
            Poll::Ready(self.current)
        } else {
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

/// A minimal single-threaded executor that blocks until a future completes.
/// This is a simplified version - real executors are much more sophisticated.
pub fn block_on<F: Future>(mut fut: F) -> F::Output {
    // Create a no-op waker (simplest possible implementation)
    unsafe fn clone(ptr: *const ()) -> RawWaker {
        RawWaker::new(ptr, &VTABLE)
    }
    unsafe fn noop(_: *const ()) {}

    static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);

    let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
    let mut cx = Context::from_waker(&waker);

    // SAFETY: We never move `fut` after pinning
    let mut fut = unsafe { Pin::new_unchecked(&mut fut) };

    // Keep polling until ready
    loop {
        match fut.as_mut().poll(&mut cx) {
            Poll::Ready(value) => return value,
            Poll::Pending => continue,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_delayed_value_immediate() {
        let future = DelayedValue::new(42, 0);
        assert_eq!(block_on(future), 42);
    }

    #[test]
    fn test_delayed_value_with_polls() {
        let future = DelayedValue::new(100, 5);
        assert_eq!(block_on(future), 100);
    }

    #[test]
    fn test_ready_immediate() {
        let future = Ready::new("hello");
        assert_eq!(block_on(future), "hello");
    }

    #[test]
    fn test_poll_counter_counts_correctly() {
        let future = PollCounter::new(3);
        assert_eq!(block_on(future), 3);
    }

    #[test]
    fn test_poll_counter_single_poll() {
        let future = PollCounter::new(1);
        assert_eq!(block_on(future), 1);
    }

    #[test]
    fn test_delayed_value_preserves_value() {
        let future1 = DelayedValue::new(-42, 2);
        let future2 = DelayedValue::new(i32::MAX, 1);
        assert_eq!(block_on(future1), -42);
        assert_eq!(block_on(future2), i32::MAX);
    }
}
(* OCaml: manual future-like with continuations *)

type 'a state = Pending of (unit -> 'a state) | Ready of 'a

let rec run = function
  | Ready v -> v
  | Pending f -> run (f ())

let delayed_value n steps =
  let rec loop i =
    if i = 0 then Ready n
    else Pending (fun () -> loop (i-1))
  in loop steps

let () =
  Printf.printf "Got: %d\n" (run (delayed_value 42 3))