ExamplesBy LevelBy TopicLearning Paths
980 Intermediate

980 Async Map

Functional Programming

Tutorial

The Problem

Demonstrate mapping over async futures in Rust — the async equivalent of Lwt.map f promise. Show that async { fut.await * 2 } is the Rust idiom for Lwt.map (fun x -> x * 2) fut. Implement typed async transformations (map_double, map_to_string) and chains of maps. Connect to the Functor typeclass: Future is a functor where map preserves the computational context.

🎯 Learning Outcomes

  • • Implement async fn map_double(fut: impl Future<Output=i32>) -> i32 as async { fut.await * 2 }
  • • Implement async fn map_to_string(fut: impl Future<Output=i32>) -> String as async { fut.await.to_string() }
  • • Chain maps: map_to_string(map_double(base_value())) — no explicit bind needed
  • • Recognize async { f(fut.await) } as the Rust equivalent of OCaml's Lwt.map f fut
  • • Understand that async fn returning a value is Lwt.return; .await is Lwt.bind
  • Code Example

    #![allow(clippy::all)]
    // 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)
                               // "10" (map)
        doubled.to_string()
    }
    
    // --- 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
    }
    
    #[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);
        }
    }

    Key Differences

    AspectRustOCaml
    Map a futureasync { fut.await * 2 }Lwt.map (fun x -> x * 2) fut
    Chain mapsCompose async fns\|> with Lwt.map
    Named mapasync fn map_double(fut) -> i32 { fut.await * 2 }let map_double = Lwt.map (fun x -> x * 2)
    Functor lawNot enforced by type systemNot enforced

    async { fut.await * 2 } is a "lifted" function application. It demonstrates that Future forms a functor: map id = id and map (g ∘ f) = map g ∘ map f hold by construction for async blocks.

    OCaml Approach

    open Lwt
    
    let base_value () = return 5
    
    (* Lwt.map: transform the result of a promise *)
    let map_double fut = Lwt.map (fun x -> x * 2) fut
    let map_to_string fut = Lwt.map string_of_int fut
    
    (* Chain maps *)
    let map_chain () =
      base_value ()
      |> map_double
      |> map_to_string
    
    (* Using let* *)
    let map_chain_letstar () =
      let* x = base_value () in
      let* y = return (x * 2) in
      return (string_of_int y)
    

    Lwt.map f p allocates a new promise that resolves with f(v) when p resolves with v. In Rust, async { p.await |> f } achieves the same without a named function. The |> pipeline in OCaml reads as "take base_value, double it, stringify it" — identical to the Rust chain.

    Full Source

    #![allow(clippy::all)]
    // 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)
                               // "10" (map)
        doubled.to_string()
    }
    
    // --- 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
    }
    
    #[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);
        }
    }
    ✓ Tests Rust test suite
    #[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);
        }
    }

    Deep Comparison

    Map over Async — Comparison

    Core Insight

    map lifts a pure function f: A -> B into async context without needing to chain two binds. 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 futureLwt.map f promiseasync { f(fut.await) }
    Infix mappromise >|= f(no built-in infix, use closure)
    Identity lawLwt.map Fun.id p = pasync { id(p.await) } = p
    Composition lawmap (f∘g) = map f ∘ map gSame via async nesting
    Map via bindbind 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::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

  • Implement a generic future_map<T, U, F: Future<Output=T>>(fut: F, f: impl FnOnce(T) -> U) -> impl Future<Output=U>.
  • Verify the functor identity law: future_map(fut, |x| x) equals fut in output value.
  • Implement future_map2<A, B, C>(fa, fb, f) — combine two independent futures with a binary function.
  • Build a pipeline of five async transformations chained with future_map.
  • Show how future_map(future_map(fut, f), g) equals future_map(fut, |x| g(f(x))) with a test.
  • Open Source Repos