๐Ÿฆ€ Functional Rust

979: Future/Promise Basics

Difficulty: Intermediate Category: Async / Concurrency FP Patterns Concept: Futures as monads โ€” `return`/`bind`/`map` in async context Key Insight: `async fn` is syntactic sugar for a state machine implementing `Future`; `.await` is monadic `bind`

Versions

DirectoryDescription
`std/`Standard library version using `std::sync`, `std::thread`
`tokio/`Tokio async runtime version using `tokio::sync`, `tokio::spawn`

Running

# Standard library version
cd std && cargo test

# Tokio version
cd tokio && cargo test
// 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
}

fn main() {
    let result = block_on(double_result());
    println!("double_result: {}", result);

    let result2 = block_on(pipeline(5));
    println!("pipeline(5): {}", result2);

    let result3 = block_on(use_manual_future());
    println!("manual future: {}", result3);
}

#[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);
    }
}
(* 979: Future/Promise Basics *)
(* OCaml: Lwt monad concept shown with pure Option/Result monads *)
(* We model the Future/Promise monad pattern without external deps *)

(* --- Approach 1: Model Future as a lazy thunk (pure simulation) --- *)

type 'a future = unit -> 'a

let return_ x : 'a future = fun () -> x

let bind (f : 'a future) (k : 'a -> 'b future) : 'b future =
  fun () -> k (f ()) ()

let map (f : 'a -> 'b) (fut : 'a future) : 'b future =
  fun () -> f (fut ())

let run fut = fut ()

let () =
  let fut = return_ 42 in
  let fut2 = bind fut (fun x -> return_ (x + 1)) in
  let fut3 = map (fun x -> x * 2) fut2 in
  assert (run fut3 = 86);
  Printf.printf "Approach 1 (lazy thunk future): %d\n" (run fut3)

(* --- Approach 2: Future as Result monad (error-aware) --- *)

type ('a, 'e) result_future = unit -> ('a, 'e) result

let ok x : ('a, 'e) result_future = fun () -> Ok x
let err e : ('a, 'e) result_future = fun () -> Error e

let bind_r (f : ('a, 'e) result_future) (k : 'a -> ('b, 'e) result_future) : ('b, 'e) result_future =
  fun () -> match f () with
    | Ok v -> k v ()
    | Error e -> Error e

let () =
  let computation =
    bind_r (ok 10) (fun x ->
    bind_r (ok 20) (fun y ->
    ok (x + y)))
  in
  (match computation () with
  | Ok v -> assert (v = 30); Printf.printf "Approach 2 (result future): %d\n" v
  | Error _ -> assert false)

(* --- Approach 3: Promise with state (mutable cell) --- *)

type 'a promise_state = Pending | Resolved of 'a

type 'a promise = { mutable state : 'a promise_state }

let make_promise () = { state = Pending }

let resolve p v = p.state <- Resolved v

let await p =
  match p.state with
  | Resolved v -> v
  | Pending -> failwith "promise not yet resolved"

let () =
  let p = make_promise () in
  resolve p 99;
  let v = await p in
  assert (v = 99);
  Printf.printf "Approach 3 (promise state): %d\n" v

let () = Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed 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 / 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)`
LazinessExplicit thunkImplicit โ€” poll-driven
Error handling`Lwt_result.t``async fn -> Result<T,E>`
Custom future`Lwt.task` + resolver`impl Future for T`

std vs tokio

Aspectstd versiontokio version
RuntimeOS 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)
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