322: The Future Trait and Poll
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
Future::poll() as returning Poll::Ready(T) or Poll::PendingFuture manually to understand the state machine modelasync fn generates a Future impl automaticallyWaker in signaling the executor to re-pollCode 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
Future is pull-based (executor calls poll); OCaml's Lwt is push-based (completion triggers callbacks).waker().wake() when it can make progress — without this, the executor won't re-poll.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);
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| State representation | ADT variant | Poll enum |
| Continuation | Closure unit -> 'a state | Waker callback |
| Executor | Recursive function | block_on loop |
| Memory safety | GC handles | Pin prevents moves |
| Zero-cost | Closure allocation | No allocation in poll |
Exercises
ReadyFuture<T> that always returns Poll::Ready(value) immediately without ever returning Pending.CountdownFuture that returns Pending exactly N times before returning Poll::Ready(()).Future to completion by calling poll repeatedly until Ready.