979 Future Basics
Tutorial
The Problem
Introduce Rust's Future trait and async/await syntax by implementing a minimal synchronous executor (block_on) using Pin, Context, and Waker. Show that async fn desugars to a state machine implementing Future, that .await is monadic bind, and that sequential async code resembles OCaml's Lwt monad with let* syntax.
🎯 Learning Outcomes
block_on<F: Future>(fut: F) -> F::Output using Pin::new_unchecked and a no-op wakerasync fn compute() -> T desugars to fn compute() -> impl Future<Output=T>.await as monadic bind: x.await extracts x's value and continues the computationasync chains: let x = f().await; let y = g(x).await; Ok(y)Lwt.bind and let* syntax for async codeCode Example
#![allow(clippy::all)]
// 979: Future/Promise Basics
// Rust async fn + await — showing the monad connection in pure std code
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
// --- A minimal synchronous executor (no tokio needed) ---
fn block_on<F: Future>(mut fut: F) -> F::Output {
// Safety: we pin the future on the stack
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
// Create a no-op waker
fn noop(_: *const ()) {}
fn noop_clone(p: *const ()) -> RawWaker {
RawWaker::new(p, &VTABLE)
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
let raw = RawWaker::new(std::ptr::null(), &VTABLE);
let waker = unsafe { Waker::from_raw(raw) };
let mut cx = Context::from_waker(&waker);
// For simple futures that resolve immediately, one poll is enough
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => v,
Poll::Pending => panic!("Future not ready — use a real executor for async I/O"),
}
}
// --- Approach 1: async fn is syntactic sugar for impl Future ---
async fn compute_value() -> i32 {
42
}
async fn compute_and_add() -> i32 {
let x = compute_value().await; // bind: unwrap the future
x + 1
}
async fn double_result() -> i32 {
let x = compute_and_add().await;
x * 2 // map: transform the value
}
// --- Approach 2: async block as lambda ---
async fn pipeline(input: i32) -> i32 {
// Sequential monadic chain via .await
let step1 = async { input * 2 }.await;
let step2 = async { step1 + 10 }.await;
let step3 = async { step2.to_string().len() as i32 }.await;
step3
}
// --- Approach 3: Manual Future implementing the trait ---
struct ImmediateFuture<T>(Option<T>);
impl<T: Unpin> Future for ImmediateFuture<T> {
type Output = T;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<T> {
Poll::Ready(self.0.take().expect("polled after completion"))
}
}
fn immediate<T>(val: T) -> ImmediateFuture<T> {
ImmediateFuture(Some(val))
}
async fn use_manual_future() -> i32 {
immediate(100).await + immediate(23).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_value() {
assert_eq!(block_on(compute_value()), 42);
}
#[test]
fn test_compute_and_add() {
assert_eq!(block_on(compute_and_add()), 43);
}
#[test]
fn test_double_result() {
assert_eq!(block_on(double_result()), 86);
}
#[test]
fn test_pipeline() {
// 5*2=10, 10+10=20, len("20")=2
assert_eq!(block_on(pipeline(5)), 2);
}
#[test]
fn test_manual_future() {
assert_eq!(block_on(use_manual_future()), 123);
}
#[test]
fn test_async_is_lazy() {
// Creating a future does NOT run it — laziness like OCaml's thunk
let _fut = compute_value(); // nothing runs here
let result = block_on(_fut);
assert_eq!(result, 42);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Async primitive | Future trait + state machine | Lwt.t promise/deferred |
| Sequencing | .await | let* / Lwt.bind |
| Parallel | tokio::join! | Lwt.both / Lwt.all |
| Runtime | tokio / async-std (external) | Lwt (external) or Eio (newer) |
| Stack pinning | Pin<&mut F> required | GC manages lifetime |
async/await in Rust compiles to zero-overhead state machines — no heap allocation per future by default. OCaml's Lwt allocates a heap promise for each deferred computation.
OCaml Approach
(* OCaml: Lwt for async *)
open Lwt.Syntax
let compute_value () = Lwt.return 42
let compute_and_add () =
let* x = compute_value () in (* x = await compute_value() *)
Lwt.return (x + 1)
(* Sequential chain *)
let full_pipeline () =
let* a = step_one () in
let* b = step_two a in
let* c = step_three b in
Lwt.return c
(* Lwt.bind is >>=; let* is syntactic sugar *)
(* Rust .await ≡ OCaml let* ≡ Haskell >>= *)
OCaml's Lwt.return ↔ async { value }. Lwt.bind p f ↔ async { f(p.await) }. The let* syntax reads identically to Rust's let x = p.await; ... — both are sequential monadic composition.
Full Source
#![allow(clippy::all)]
// 979: Future/Promise Basics
// Rust async fn + await — showing the monad connection in pure std code
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
// --- A minimal synchronous executor (no tokio needed) ---
fn block_on<F: Future>(mut fut: F) -> F::Output {
// Safety: we pin the future on the stack
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
// Create a no-op waker
fn noop(_: *const ()) {}
fn noop_clone(p: *const ()) -> RawWaker {
RawWaker::new(p, &VTABLE)
}
static VTABLE: RawWakerVTable = RawWakerVTable::new(noop_clone, noop, noop, noop);
let raw = RawWaker::new(std::ptr::null(), &VTABLE);
let waker = unsafe { Waker::from_raw(raw) };
let mut cx = Context::from_waker(&waker);
// For simple futures that resolve immediately, one poll is enough
match fut.as_mut().poll(&mut cx) {
Poll::Ready(v) => v,
Poll::Pending => panic!("Future not ready — use a real executor for async I/O"),
}
}
// --- Approach 1: async fn is syntactic sugar for impl Future ---
async fn compute_value() -> i32 {
42
}
async fn compute_and_add() -> i32 {
let x = compute_value().await; // bind: unwrap the future
x + 1
}
async fn double_result() -> i32 {
let x = compute_and_add().await;
x * 2 // map: transform the value
}
// --- Approach 2: async block as lambda ---
async fn pipeline(input: i32) -> i32 {
// Sequential monadic chain via .await
let step1 = async { input * 2 }.await;
let step2 = async { step1 + 10 }.await;
let step3 = async { step2.to_string().len() as i32 }.await;
step3
}
// --- Approach 3: Manual Future implementing the trait ---
struct ImmediateFuture<T>(Option<T>);
impl<T: Unpin> Future for ImmediateFuture<T> {
type Output = T;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<T> {
Poll::Ready(self.0.take().expect("polled after completion"))
}
}
fn immediate<T>(val: T) -> ImmediateFuture<T> {
ImmediateFuture(Some(val))
}
async fn use_manual_future() -> i32 {
immediate(100).await + immediate(23).await
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_value() {
assert_eq!(block_on(compute_value()), 42);
}
#[test]
fn test_compute_and_add() {
assert_eq!(block_on(compute_and_add()), 43);
}
#[test]
fn test_double_result() {
assert_eq!(block_on(double_result()), 86);
}
#[test]
fn test_pipeline() {
// 5*2=10, 10+10=20, len("20")=2
assert_eq!(block_on(pipeline(5)), 2);
}
#[test]
fn test_manual_future() {
assert_eq!(block_on(use_manual_future()), 123);
}
#[test]
fn test_async_is_lazy() {
// Creating a future does NOT run it — laziness like OCaml's thunk
let _fut = compute_value(); // nothing runs here
let result = block_on(_fut);
assert_eq!(result, 42);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_value() {
assert_eq!(block_on(compute_value()), 42);
}
#[test]
fn test_compute_and_add() {
assert_eq!(block_on(compute_and_add()), 43);
}
#[test]
fn test_double_result() {
assert_eq!(block_on(double_result()), 86);
}
#[test]
fn test_pipeline() {
// 5*2=10, 10+10=20, len("20")=2
assert_eq!(block_on(pipeline(5)), 2);
}
#[test]
fn test_manual_future() {
assert_eq!(block_on(use_manual_future()), 123);
}
#[test]
fn test_async_is_lazy() {
// Creating a future does NOT run it — laziness like OCaml's thunk
let _fut = compute_value(); // nothing runs here
let result = block_on(_fut);
assert_eq!(result, 42);
}
}
Deep Comparison
Future/Promise Basics — Comparison
Core Insight
Both OCaml's Lwt and Rust's async/await express the Future monad: a computation that produces a value later. The monad laws hold: return wraps a value, bind chains computations, map transforms results.
OCaml Approach
Lwt.return x wraps a value in an already-resolved promiseLwt.bind p f (or let*) chains: when p resolves, pass result to fLwt.map f p transforms the resolved value with funit -> 'a thunks (lazy evaluation)Lwt_main.run drives the event loop to completionRust Approach
async fn creates a state machine implementing Future.await is desugared bind: suspend until the sub-future resolvesasync { expr } is an async block (anonymous future)block_on executor can drive immediate futures without tokioComparison Table
| Concept | OCaml (Lwt) | Rust |
|---|---|---|
| Return / wrap | Lwt.return x | async { x } or ready future |
| Bind / chain | Lwt.bind p f / let* | p.await inside async fn |
| Map / transform | Lwt.map f p | async { f(p.await) } |
| Run / execute | Lwt_main.run p | executor::block_on(f) |
| Laziness | Explicit thunk | Implicit — poll-driven |
| Error handling | Lwt_result.t | async fn -> Result<T,E> |
| Custom future | Lwt.task + resolver | impl Future for T |
std vs tokio
| Aspect | std version | tokio version |
|---|---|---|
| Runtime | OS threads via std::thread | Async tasks on tokio runtime |
| Synchronization | std::sync::Mutex, Condvar | tokio::sync::Mutex, channels |
| Channels | std::sync::mpsc (unbounded) | tokio::sync::mpsc (bounded, async) |
| Blocking | Thread blocks on lock/recv | Task yields, runtime switches tasks |
| Overhead | One OS thread per task | Many tasks per thread (M:N) |
| Best for | CPU-bound, simple concurrency | I/O-bound, high-concurrency servers |
Exercises
async fn pipeline(x: i32) -> String that chains three async transformations sequentially.async_map<T, U, F: Future<Output=T>>(fut: F, f: impl Fn(T) -> U) -> U as async { f(fut.await) }.async_and_then<T, U>(fut: impl Future<Output=T>, f: impl Fn(T) -> impl Future<Output=U>) -> U.async { f(x).await } equals f(x) for pure f.tokio or smol) and rewrite compute_and_add using it.