Skip to content

← Async From First Principles step 3 of 25

Easy Primitives

The Future trait: Output, Poll, and your first hand-written future

A future is a value that represents work which may not have finished yet. In Rust, a future is any type that implements the Future trait from std::future. An async fn returns a value of a compiler-generated type that implements this trait, and .await drives a future by calling the method below. Leaving out its attributes, the trait has had this shape since it became stable in Rust 1.36:

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

type Output is an associated type: each implementation names the type of value the future produces when it finishes. poll is the only method.

In this exercise you implement the trait by hand for two types and then poll them yourself.

pub struct Immediate<T>(pub Option<T>);   // finishes on its first poll
pub struct NeverDone;                     // never finishes

pub fn probe(vals: Vec<i32>, poll_never: bool) -> Vec<Option<i32>>

The contract

  • Immediate<T> has Output = T. Its first poll returns Poll::Ready(value), taking the value out of the Option.
  • NeverDone has Output = i32. Every poll returns Poll::Pending.
  • probe goes through vals in order. For each value v it creates a new Immediate(Some(v)), polls it exactly once, and pushes Some(x) if the poll returned Ready(x) or None if it returned Pending.
  • After that, if poll_never is true, probe creates one NeverDone, polls it once, and pushes the result in the same way. If poll_never is false, nothing more is pushed.

So the result is every value of vals, in its original order and unchanged, followed by one None when poll_never is true. vals may be empty, and its values can be any i32, including i32::MIN and i32::MAX. The grader shows the result as a JSON array in which None appears as null: for vals = [1, 2, 3] and poll_never = true the expected output is [1, 2, 3, null], and for an empty vals with poll_never = true it is [null].

How to read poll

poll asks the future to make as much progress as it can right now, without blocking the thread, and to report whether it has finished. It returns this enum from std::task (note: Poll is in std::task, not std::future):

enum Poll<T> { Ready(T), Pending }

Ready(value) means the future has finished and hands over its output. Pending means it has not finished yet and the caller should poll again later.

The two parameters need only a short explanation for now:

  • self: Pin<&mut Self> is a mutable reference wrapped in Pin. Pin guarantees that, once a value is pinned, it stays at the same memory address until it is dropped, unless its type implements the Unpin trait. Unpin means the type does not depend on staying at one address, so moving it is harmless. Unpin is an auto trait: the compiler implements it for a struct automatically when every field is Unpin. NeverDone has no fields, so it is Unpin; Immediate<T> is Unpin whenever T is. For an Unpin type, Pin<&mut Self> lets you read and modify fields as if it were &mut Self, which is why self.0.take() works here. A later item in this track explains what pinning is for.
  • cx: &mut Context<'_> carries the waker, the handle a future uses to ask to be polled again once it can make progress. Neither future here uses it: Immediate is finished at once and NeverDone never will be. Later items in this track build futures that do.

Building a Context to poll with

To call poll you need a Context, and Context::from_waker builds one from a &Waker. The standard library provides a waker that does nothing when asked to wake a task, which is all this exercise needs:

let mut cx = Context::from_waker(Waker::noop());

Waker::noop() is stable since Rust 1.85 and already returns a &'static Waker, so it can be passed directly.

Polling a future by hand

poll takes Pin<&mut Self>, not &mut self, so you first wrap a mutable reference in Pin. Pin::new does this safely, but only for Unpin types; for any other type it fails to compile with E0277. It is only a wrapper around the reference and does not move or copy the future:

let mut fut = Immediate(Some(7));
let p = Pin::new(&mut fut).poll(&mut cx);   // Poll::Ready(7)

Calling poll as a method requires the Future trait to be in scope. In the 2024 edition it is in the prelude, so this works even without the starter's use std::future::Future;; in the 2018 and 2021 editions that import is required.

The compiler error in the starter

The starter's impl Future for Immediate<T> does not say what Output is, so it fails to compile with E0046, "not all trait items implemented, missing: Output". An implementation must define every associated type the trait declares. Immediate<T> produces a T. NeverDone never produces a value, but its implementation still has to name an Output type; the starter already uses i32.

The starter's impl also has the bound T: Unpin. Remove it and the compiler rejects self.0.take() with E0596, because modifying a field through Pin<&mut Self> is only allowed when Self is Unpin.

Polling after Ready

Immediate hands out its value with Option::take, which leaves None behind, so a second poll reaches the expect and panics. That is allowed. The Future documentation says that once a future has returned Ready, callers should not poll it again, and that if they do, the future may panic, never finish, or misbehave in some other way, as long as it does not cause undefined behavior. probe polls each Immediate only once, so the panic never happens in this exercise.