ExamplesBy LevelBy TopicLearning Paths
979 Fundamental

979 Future Basics

Functional Programming

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

  • • Implement block_on<F: Future>(fut: F) -> F::Output using Pin::new_unchecked and a no-op waker
  • • Understand that async fn compute() -> T desugars to fn compute() -> impl Future<Output=T>
  • • Recognize .await as monadic bind: x.await extracts x's value and continues the computation
  • • Implement sequential async chains: let x = f().await; let y = g(x).await; Ok(y)
  • • Understand the connection to OCaml's Lwt.bind and let* syntax for async code
  • Code 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

    AspectRustOCaml
    Async primitiveFuture trait + state machineLwt.t promise/deferred
    Sequencing.awaitlet* / Lwt.bind
    Paralleltokio::join!Lwt.both / Lwt.all
    Runtimetokio / async-std (external)Lwt (external) or Eio (newer)
    Stack pinningPin<&mut F> requiredGC 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.returnasync { value }. Lwt.bind p fasync { 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);
        }
    }
    ✓ Tests Rust test suite
    #[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 promise
  • Lwt.bind p f (or let*) chains: when p resolves, pass result to f
  • Lwt.map f p transforms the resolved value with f
  • • Simulated here as unit -> 'a thunks (lazy evaluation)
  • Lwt_main.run drives the event loop to completion
  • Rust Approach

  • async fn creates a state machine implementing Future
  • .await is desugared bind: suspend until the sub-future resolves
  • async { expr } is an async block (anonymous future)
  • • Futures are lazy — nothing runs until polled by an executor
  • • A minimal block_on executor can drive immediate futures without tokio
  • Comparison Table

    ConceptOCaml (Lwt)Rust
    Return / wrapLwt.return xasync { x } or ready future
    Bind / chainLwt.bind p f / let*p.await inside async fn
    Map / transformLwt.map f pasync { f(p.await) }
    Run / executeLwt_main.run pexecutor::block_on(f)
    LazinessExplicit thunkImplicit — poll-driven
    Error handlingLwt_result.tasync fn -> Result<T,E>
    Custom futureLwt.task + resolverimpl Future for T

    std vs tokio

    Aspectstd versiontokio version
    RuntimeOS threads via std::threadAsync tasks on tokio runtime
    Synchronizationstd::sync::Mutex, Condvartokio::sync::Mutex, channels
    Channelsstd::sync::mpsc (unbounded)tokio::sync::mpsc (bounded, async)
    BlockingThread blocks on lock/recvTask yields, runtime switches tasks
    OverheadOne OS thread per taskMany tasks per thread (M:N)
    Best forCPU-bound, simple concurrencyI/O-bound, high-concurrency servers

    Exercises

  • Write an async fn pipeline(x: i32) -> String that chains three async transformations sequentially.
  • Implement async_map<T, U, F: Future<Output=T>>(fut: F, f: impl Fn(T) -> U) -> U as async { f(fut.await) }.
  • Implement async_and_then<T, U>(fut: impl Future<Output=T>, f: impl Fn(T) -> impl Future<Output=U>) -> U.
  • Verify the monad left-identity law: async { f(x).await } equals f(x) for pure f.
  • Add a real runtime dependency (tokio or smol) and rewrite compute_and_add using it.
  • Open Source Repos