980 Async Map
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
async fn map_double(fut: impl Future<Output=i32>) -> i32 as async { fut.await * 2 }async fn map_to_string(fut: impl Future<Output=i32>) -> String as async { fut.await.to_string() }map_to_string(map_double(base_value())) â no explicit bind neededasync { f(fut.await) } as the Rust equivalent of OCaml's Lwt.map f futasync fn returning a value is Lwt.return; .await is Lwt.bindCode 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
| Aspect | Rust | OCaml |
|---|---|---|
| Map a future | async { fut.await * 2 } | Lwt.map (fun x -> x * 2) fut |
| Chain maps | Compose async fns | \|> with Lwt.map |
| Named map | async fn map_double(fut) -> i32 { fut.await * 2 } | let map_double = Lwt.map (fun x -> x * 2) |
| Functor law | Not enforced by type system | Not 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);
}
}#[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 blockingmap Fun.id = Fun.id, map (f â g) = map f â map gpromise |> Lwt.map f |> Lwt.map gLwt.( >|= ) as infix map operatorRust Approach
async { f(fut.await) } is the idiomatic inline mapasync fn map(fut, f) -> U { f(fut.await) }async/await is pure transformationComparison Table
| Concept | OCaml (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) } |
| Allocation | Lwt promise allocation | Zero-cost state machine |
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
future_map<T, U, F: Future<Output=T>>(fut: F, f: impl FnOnce(T) -> U) -> impl Future<Output=U>.future_map(fut, |x| x) equals fut in output value.future_map2<A, B, C>(fa, fb, f) â combine two independent futures with a binary function.future_map.future_map(future_map(fut, f), g) equals future_map(fut, |x| g(f(x))) with a test.