ExamplesBy LevelBy TopicLearning Paths
529 Intermediate

Async Closures

Functional Programming

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

  • • How |x| async { ... } produces a closure returning an anonymous Future
  • • How F: FnOnce(T) -> Fut, Fut: Future<Output = U> bounds express async callbacks
  • • How to implement async_map and async_filter over collections using sequential await
  • • Why true async fn closures require nightly and what the stable workaround looks like
  • • Where async callbacks appear: middleware chains, retry logic, async iterators (streams)
  • Code 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

  • Syntax: Rust's .await is a postfix operator; OCaml/Lwt uses >>= infix or let* binding syntax — both express sequential async composition.
  • True async closures: Rust stable requires the workaround |x| async { ... } returning a Future; OCaml functions returning Lwt.t are the natural async closure form with no special syntax needed.
  • Parallelism control: Rust's async_map processes sequentially by default; switching to futures::join_all enables parallelism; OCaml's Lwt_list.map_p enables parallel futures explicitly.
  • Type complexity: Rust async callbacks introduce two generic parameters (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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • OCaml: Lwt/Async libraries provide async primitives
  • Rust: async/await is built into the language
  • Rust: |x| async move { } pattern for async closures
  • Rust: True async closures (async |x| {}) are nightly-only
  • Both: Closures can return async computations
  • Exercises

  • Parallel async map: Rewrite async_map using futures::future::join_all to process items concurrently instead of sequentially, and verify both produce the same output.
  • Retry with async: Implement 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: Implement 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.
  • Open Source Repos