Async Closures
Tutorial Video
Text description (accessibility)
This video demonstrates the "Async Closures" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Async programming requires composing not just values but futures — asynchronous computations that may yield before completing. Key difference from OCaml: 1. **Syntax**: Rust's `.await` is a postfix operator; OCaml/Lwt uses `>>=` infix or `let*` binding syntax — both express sequential async composition.
Tutorial
The Problem
Async programming requires composing not just values but futures — asynchronous computations that may yield before completing. A common need is passing callbacks that themselves perform async work: an HTTP client that accepts an async retry handler, a task queue that calls an async processing function per item, or a middleware chain where each layer can await I/O. True async |x| { ... } closure syntax is nightly-only in Rust; the stable pattern uses |x| async move { ... } — a closure returning a Future.
🎯 Learning Outcomes
|x| async { ... } produces a closure returning an anonymous FutureF: FnOnce(T) -> Fut, Fut: Future<Output = U> bounds express async callbacksasync_map and async_filter over collections using sequential awaitasync fn closures require nightly and what the stable workaround looks likeCode Example
// Closure returning a future (stable pattern)
let double = |x: i32| async move { x * 2 };
// Async map
pub async fn async_map<T, U, F, Fut>(items: Vec<T>, f: F) -> Vec<U>
where F: Fn(T) -> Fut, Fut: Future<Output = U> {
let mut results = Vec::new();
for item in items { results.push(f(item).await); }
results
}Key Differences
.await is a postfix operator; OCaml/Lwt uses >>= infix or let* binding syntax — both express sequential async composition.|x| async { ... } returning a Future; OCaml functions returning Lwt.t are the natural async closure form with no special syntax needed.async_map processes sequentially by default; switching to futures::join_all enables parallelism; OCaml's Lwt_list.map_p enables parallel futures explicitly.F and Fut), making signatures verbose; OCaml's 'a -> 'b Lwt.t type is concise and uniform.OCaml Approach
OCaml 5.x uses effect handlers and Eio or Lwt for async programming. An async callback in Lwt is a function returning 'a Lwt.t:
let async_map f items =
Lwt_list.map_s f items (* map_s = sequential, map_p = parallel *)
let async_filter f items =
Lwt_list.filter_s f items
Lwt's >>= (bind) and let* syntax serve the same purpose as Rust's .await.
Full Source
#![allow(clippy::all)]
//! Async Closures
//!
//! Patterns for async callbacks using closures that return Futures.
//! Note: True `async |x| {...}` is nightly-only; we use `|x| async { ... }`.
use std::future::Future;
/// Async transform: closure returns a future.
pub fn async_transform<T, U, F, Fut>(value: T, f: F) -> impl Future<Output = U>
where
F: FnOnce(T) -> Fut,
Fut: Future<Output = U>,
{
f(value)
}
/// Demonstrate async closure pattern (returns future).
pub async fn process_with_callback<T, F, Fut>(value: T, callback: F) -> T
where
F: FnOnce(&T) -> Fut,
Fut: Future<Output = ()>,
{
callback(&value).await;
value
}
/// Async map over a collection.
pub async fn async_map<T, U, F, Fut>(items: Vec<T>, f: F) -> Vec<U>
where
F: Fn(T) -> Fut,
Fut: Future<Output = U>,
{
let mut results = Vec::with_capacity(items.len());
for item in items {
results.push(f(item).await);
}
results
}
/// Async filter.
pub async fn async_filter<T, F, Fut>(items: Vec<T>, predicate: F) -> Vec<T>
where
F: Fn(&T) -> Fut,
Fut: Future<Output = bool>,
{
let mut results = Vec::new();
for item in items {
if predicate(&item).await {
results.push(item);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_async_transform_compiles() {
// Just verify the types work - actual async test would need runtime
let _future = async_transform(5, |x| async move { x * 2 });
}
#[test]
fn test_closure_returning_future() {
// Pattern: |x| async move { ... }
let double = |x: i32| async move { x * 2 };
let _fut = double(5);
// In real code: assert_eq!(fut.await, 10);
}
#[test]
fn test_async_map_compiles() {
let _future = async_map(vec![1, 2, 3], |x| async move { x * 2 });
}
#[test]
fn test_async_filter_compiles() {
let _future = async_filter(vec![1, 2, 3, 4], |x| {
let x = *x;
async move { x % 2 == 0 }
});
}
// Block-on test requires a runtime, showing pattern only
#[test]
fn test_pattern_demonstration() {
// This demonstrates the closure-returning-future pattern
let make_doubler = || |x: i32| async move { x * 2 };
let doubler = make_doubler();
let _fut = doubler(21);
// With runtime: assert_eq!(fut.await, 42);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_async_transform_compiles() {
// Just verify the types work - actual async test would need runtime
let _future = async_transform(5, |x| async move { x * 2 });
}
#[test]
fn test_closure_returning_future() {
// Pattern: |x| async move { ... }
let double = |x: i32| async move { x * 2 };
let _fut = double(5);
// In real code: assert_eq!(fut.await, 10);
}
#[test]
fn test_async_map_compiles() {
let _future = async_map(vec![1, 2, 3], |x| async move { x * 2 });
}
#[test]
fn test_async_filter_compiles() {
let _future = async_filter(vec![1, 2, 3, 4], |x| {
let x = *x;
async move { x % 2 == 0 }
});
}
// Block-on test requires a runtime, showing pattern only
#[test]
fn test_pattern_demonstration() {
// This demonstrates the closure-returning-future pattern
let make_doubler = || |x: i32| async move { x * 2 };
let doubler = make_doubler();
let _fut = doubler(21);
// With runtime: assert_eq!(fut.await, 42);
}
}
Deep Comparison
OCaml vs Rust: Async Closures
OCaml (Lwt)
open Lwt.Infix
(* Async closure pattern *)
let async_map f items =
Lwt_list.map_p f items
let double x = Lwt.return (x * 2)
let _ = async_map double [1; 2; 3]
Rust
// Closure returning a future (stable pattern)
let double = |x: i32| async move { x * 2 };
// Async map
pub async fn async_map<T, U, F, Fut>(items: Vec<T>, f: F) -> Vec<U>
where F: Fn(T) -> Fut, Fut: Future<Output = U> {
let mut results = Vec::new();
for item in items { results.push(f(item).await); }
results
}
Key Differences
|x| async move { } pattern for async closuresasync |x| {}) are nightly-onlyExercises
async_map using futures::future::join_all to process items concurrently instead of sequentially, and verify both produce the same output.retry_async<F, Fut, T>(attempts: usize, f: F) -> impl Future<Output = Result<T, String>> where F: Fn() -> Fut, Fut: Future<Output = Result<T, String>>.async_fold<T, U, F, Fut>(items: Vec<T>, init: U, f: F) -> U where F: Fn(U, T) -> Fut, Fut: Future<Output = U> that accumulates asynchronously.