Skip to Content
Course content

146: The Future Trait Explained

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're at one of those fast-casual restaurants where you order at a counter, and instead of them calling your name, they hand you a little plastic buzzer. That buzzer is essentially a Future.

Now, think about how that buzzer actually works. It doesn't contain your burger; it's just a placeholder. It doesn't even "do" anything while it's sitting on the table. The kitchen is doing the work, and the buzzer is just the mechanism that tells you when the work is done. You don't stand at the counter asking "Is it ready?" every five seconds—that would be a waste of your time and the cashier's. Instead, you go sit down, relax, and wait for the buzzer to vibrate. When it does, you go back to the counter, trade the buzzer for your food, and finally have your meal.

Mapping the Buzzer to the Trait

In Rust, the Future trait is that plastic buzzer. When you mark a function as async, Rust doesn't actually execute the code inside immediately. Instead, it wraps that code into a state machine that implements the Future trait. Let's map the restaurant experience directly to the technical mechanics:

  • The Buzzer: This is the Future object. It represents a value that will eventually be available.
  • Checking the Food: This is the poll() method. The runtime (the executor) calls poll to see if the future has finished.
  • "Not ready yet": This is Poll::Pending. When the executor polls the future and the work isn't done, it returns this value.
  • "Here is your burger": This is Poll::Ready(val). The computation is complete, and the result is returned.
  • The Vibration: This is the Waker. This is the most critical part. The future doesn't just sit there; it has a Waker that tells the executor, "Hey, I'm actually ready now, please poll me again!"

The Laziness of Rust's Async

I want to highlight something that trips up a lot of people coming from JavaScript or C#. In those languages, a Promise or Task starts running the moment it's created. Rust is different. Rust futures are lazy.

If you call an async function in Rust, nothing happens. Literally nothing. You've just created a state machine that is sitting at its first step. It won't move a single inch until something polls it. This is why you can't just call an async function and expect it to run in the background; you need an executor (like Tokio or async-std) to actually drive the future to completion.

Looking Under the Hood

While we usually use async/await, it's helpful to see what the trait actually looks like. I've written a simplified version of a timer future here to show you how poll and Waker interact. I'm skipping the complex reactor logic for brevity, but pay attention to the return types.

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

struct SimpleTimer {
    deadline: Instant,
}

impl Future for SimpleTimer {
    type Output = String;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if Instant::now() >= self.deadline {
            // The burger is ready!
            Poll::Ready("Timer Finished!".to_string())
        } else {
            // Not ready. We tell the waker to wake us up later.
            // In a real scenario, the OS or a timer wheel would handle this.
            cx.waker().wake_by_ref(); 
            Poll::Pending
        }
    }
}

In the example above, the poll method is the heart of the operation. Notice the Pin wrapper? I know we'll dive deeper into pinning later, but for now, just know that because futures often contain references to their own data (self-referential structs), they can't be moved in memory once they start being polled. Pinning "pins" them in place so those references don't become invalid.

Where the Executor Fits In

If the Future is the buzzer, the Executor is the entire restaurant system. The executor maintains a list of all the futures that are currently "pending." It doesn't loop through them constantly (which would kill your CPU); it sleeps until a Waker signals that a specific future is ready to be polled again. This efficiency is why Rust can handle tens of thousands of concurrent connections on a single thread without breaking a sweat.




📋 Practical Task

Implementing a Manual Delayed-Value Future

Your task is to create a custom Future called DelayedValue. Instead of using async { ... } blocks, you must implement the Future trait manually for a struct.

Requirements:

  • Create a struct DelayedValue that holds a count (integer) and a target (integer).
  • Implement Future for DelayedValue.
  • The Output type should be String.
  • In the poll method:
    • If count is less than target, increment count, call cx.waker().wake_by_ref(), and return Poll::Pending.
    • If count has reached target, return Poll::Ready with a string saying "Reached target!".

Hint: You will need to import std::pin::Pin, std::task::{Context, Poll}, and std::future::Future. You can test this by using futures::executor::block_on or a similar executor to run your future to completion.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.