๐Ÿฆ€ Functional Rust

980: Map over Async

Difficulty: Beginner Category: Async / Concurrency FP Patterns Concept: Functor `map` over futures โ€” transform output without unwrapping manually Key Insight: `async { f(fut.await) }` is the direct Rust equivalent of `Lwt.map f fut`; satisfies functor laws

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
// 980: Map over Async
// Rust: async { f(x.await) } is the idiom for Lwt.map f promise

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};

fn block_on<F: Future>(mut fut: F) -> F::Output {
    let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
    fn noop(_: *const ()) {}
    fn clone(p: *const ()) -> RawWaker { RawWaker::new(p, &VT) }
    static VT: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
    let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VT)) };
    let mut cx = Context::from_waker(&waker);
    match fut.as_mut().poll(&mut cx) {
        Poll::Ready(v) => v,
        Poll::Pending => panic!("not ready"),
    }
}

// The base future
async fn base_value() -> i32 { 5 }

// --- map: transform the output of a future ---
// Lwt.map (fun x -> x * 2) fut  โ‰ก  async { fut.await * 2 }
async fn map_double(fut: impl Future<Output = i32>) -> i32 {
    fut.await * 2
}

async fn map_to_string(fut: impl Future<Output = i32>) -> String {
    fut.await.to_string()
}

// --- Functor-style: compose maps ---
async fn map_chain() -> String {
    let raw = base_value().await;          // 5
    let doubled = raw * 2;                 // 10  (map)
    let as_str = doubled.to_string();      // "10" (map)
    as_str
}

// --- map derived from bind (async block = bind + return) ---
async fn map_via_bind<T, U, F>(fut: impl Future<Output = T>, f: F) -> U
where
    F: FnOnce(T) -> U,
{
    // .await is bind, wrapping in async is return
    f(fut.await)
}

// --- Functor laws ---
async fn identity_law() -> bool {
    let val = base_value().await;
    let mapped = async { base_value().await }.await; // map id
    val == mapped
}

async fn composition_law() -> bool {
    let f = |x: i32| x + 1;
    let g = |x: i32| x * 3;

    // map (f . g) fut
    let composed = async { f(g(base_value().await)) }.await;
    // map f (map g fut)
    let chained = async { f(async { g(base_value().await) }.await) }.await;
    composed == chained
}

fn main() {
    let s = block_on(map_chain());
    println!("map_chain: {}", s);

    let r = block_on(map_via_bind(base_value(), |x| x * x));
    println!("map_via_bind (square): {}", r);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_map_double() {
        assert_eq!(block_on(map_double(base_value())), 10);
    }

    #[test]
    fn test_map_to_string() {
        assert_eq!(block_on(map_to_string(base_value())), "5");
    }

    #[test]
    fn test_map_chain() {
        assert_eq!(block_on(map_chain()), "10");
    }

    #[test]
    fn test_map_via_bind() {
        assert_eq!(block_on(map_via_bind(base_value(), |x| x * x)), 25);
    }

    #[test]
    fn test_identity_law() {
        assert!(block_on(identity_law()));
    }

    #[test]
    fn test_composition_law() {
        assert!(block_on(composition_law()));
    }

    #[test]
    fn test_inline_map() {
        // Inline Lwt.map style
        let result = block_on(async { base_value().await + 100 });
        assert_eq!(result, 105);
    }
}
(* 980: Map over Async *)
(* OCaml: Lwt.map f promise โ†’ transform a resolved value *)

(* --- Approach 1: map over a simple future thunk --- *)

type 'a future = unit -> 'a

let return_ x : 'a future = fun () -> x
let map f fut = fun () -> f (fut ())
let bind fut k = fun () -> k (fut ()) ()
let run f = f ()

let () =
  let fut = return_ 5 in
  let doubled = map (fun x -> x * 2) fut in
  let stringed = map string_of_int doubled in
  assert (run doubled = 10);
  assert (run stringed = "10");
  Printf.printf "Approach 1 (map chain): %s\n" (run stringed)

(* --- Approach 2: functor laws on future --- *)
(* map id = id, map (f . g) = map f . map g *)

let () =
  let fut = return_ 42 in
  (* map id law *)
  let id_mapped = map (fun x -> x) fut in
  assert (run id_mapped = run fut);
  (* composition law *)
  let f x = x + 1 in
  let g x = x * 3 in
  let composed = map (fun x -> f (g x)) fut in
  let chained  = map f (map g fut) in
  assert (run composed = run chained);
  Printf.printf "Approach 2 (functor laws): โœ“\n"

(* --- Approach 3: map as derived from bind --- *)

let map_from_bind f fut =
  bind fut (fun x -> return_ (f x))

let () =
  let fut = return_ 7 in
  let r1 = map (fun x -> x * x) fut in
  let r2 = map_from_bind (fun x -> x * x) fut in
  assert (run r1 = run r2);
  Printf.printf "Approach 3 (map via bind): %d = %d\n" (run r1) (run r2)

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

๐Ÿ“Š Detailed Comparison

Map over Async โ€” Comparison

Core Insight

`map` lifts a pure function `f: A -> B` into async context without needing to chain two `bind`s. In both OCaml and Rust it's a derived operation: `map f m = bind m (fun x -> return (f x))`.

OCaml Approach

  • `Lwt.map f promise` transforms the resolved value without blocking
  • Satisfies functor laws: `map Fun.id = Fun.id`, `map (f โˆ˜ g) = map f โˆ˜ map g`
  • Can be composed in a pipeline: `promise |> Lwt.map f |> Lwt.map g`
  • Lwt also provides `Lwt.( >|= )` as infix map operator

Rust Approach

  • `async { f(fut.await) }` is the idiomatic inline map
  • Can be a helper `async fn map(fut, f) -> U { f(fut.await) }`
  • Functor laws hold because `async`/`await` is pure transformation
  • No allocation beyond the state machine

Comparison Table

ConceptOCaml (Lwt)Rust
Map a future`Lwt.map f promise``async { f(fut.await) }`
Infix map`promise >= f`(no built-in infix, use closure)
Identity law`Lwt.map Fun.id p = p``async { id(p.await) } = p`
Composition law`map (fโˆ˜g) = map f โˆ˜ map g`Same via async nesting
Map via bind`bind p (fun x -> return f x)``async { f(p.await) }`
AllocationLwt promise allocationZero-cost state machine

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