ExamplesBy LevelBy TopicLearning Paths
322 Advanced

322: The Future Trait and Poll

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "322: The Future Trait and Poll" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. The `async`/`.await` syntax in Rust is syntactic sugar over the `Future` trait. Key difference from OCaml: 1. **Poll vs callback**: Rust's `Future` is pull

Tutorial

The Problem

The async/.await syntax in Rust is syntactic sugar over the Future trait. A Future is a state machine with a single poll() method that either returns Poll::Ready(output) or Poll::Pending. Understanding the underlying Future trait is essential for implementing custom async primitives, debugging async code, and understanding why .await cannot be used in non-async contexts. This is the foundation that all async Rust is built on.

🎯 Learning Outcomes

  • • Understand Future::poll() as returning Poll::Ready(T) or Poll::Pending
  • • Implement a custom Future manually to understand the state machine model
  • • Recognize that async fn generates a Future impl automatically
  • • Understand the role of Waker in signaling the executor to re-poll
  • Code Example

    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) }
            else { self.remaining -= 1; cx.waker().wake_by_ref(); Poll::Pending }
        }
    }

    Key Differences

  • Poll vs callback: Rust's Future is pull-based (executor calls poll); OCaml's Lwt is push-based (completion triggers callbacks).
  • Zero-cost: Rust's state machine generation produces zero-allocation futures (often); Lwt uses heap-allocated closures for continuations.
  • Waker contract: Rust requires the future to call waker().wake() when it can make progress — without this, the executor won't re-poll.
  • Composability: Both models compose well for concurrent execution; Rust's model allows more compiler optimization due to its static nature.
  • OCaml Approach

    OCaml's Lwt uses continuations (callbacks) rather than a poll-based model. A Lwt "promise" is fulfilled when a callback is registered:

    (* Lwt: promise-based rather than poll-based *)
    let delayed_value n =
      let p, r = Lwt.wait () in
      Lwt.on_success (Lwt_unix.sleep 0.1) (fun () -> Lwt.wakeup r n);
      p
    

    OCaml 5's Effect system provides even lower-level primitives for custom async runtimes.

    Full Source

    #![allow(clippy::all)]
    //! # 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);
        }
    }
    ✓ Tests Rust test suite
    #[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);
        }
    }

    Deep Comparison

    OCaml vs Rust: Future Trait

    Core State Machine

    OCaml (manual):

    type 'a state = Pending of (unit -> 'a state) | Ready of 'a
    
    let rec run = function
      | Ready v -> v
      | Pending f -> run (f ())
    

    Rust:

    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) }
            else { self.remaining -= 1; cx.waker().wake_by_ref(); Poll::Pending }
        }
    }
    

    Delayed Value Creation

    OCaml:

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

    Rust:

    impl DelayedValue {
        fn new(value: i32, polls: u32) -> Self {
            Self { value, remaining: polls }
        }
    }
    

    Key Differences

    AspectOCamlRust
    State representationADT variantPoll enum
    ContinuationClosure unit -> 'a stateWaker callback
    ExecutorRecursive functionblock_on loop
    Memory safetyGC handlesPin prevents moves
    Zero-costClosure allocationNo allocation in poll

    Exercises

  • Implement a ReadyFuture<T> that always returns Poll::Ready(value) immediately without ever returning Pending.
  • Implement a CountdownFuture that returns Pending exactly N times before returning Poll::Ready(()).
  • Write a simple single-threaded executor that drives a Future to completion by calling poll repeatedly until Ready.
  • Open Source Repos