323: async blocks and Lazy Evaluation
Difficulty: 3 Level: Advanced An `async { }` block creates an anonymous Future inline โ lazy by default, nothing runs until it's awaited or driven.The Problem This Solves
Sometimes you want to create a piece of async work without defining a whole named function for it. Maybe you're building a list of tasks dynamically, or you need to capture some local variables into a future that will run later. Named `async fn` works for reusable operations, but for one-off deferred computations, it's too heavy. More importantly, lazy evaluation is a superpower in concurrent programming. If you can describe work without starting it, you can decide whether to run it, when to run it, and which of several options to run. Eagerly starting work you might not need wastes resources and can cause race conditions. This is the foundation of `select!` (race two futures, discard the loser) and conditional execution โ patterns that only work because Futures don't start until polled.The Intuition
In Rust, `async { }` is to functions what closures are to named functions โ anonymous, inline, capturing their environment. The difference from a regular closure: it returns a `Future`, not a value. Think of it like a thunk in functional programming: `fun () -> expensive_computation()` in OCaml, or a lazy `val` in Haskell. The work is described but not done. In JavaScript, you'd write `() => Promise.resolve(compute())`. The outer arrow function is the "wrap it in a lazy container" part. In Rust, `async { compute() }` does the same thing โ but the laziness is guaranteed by the type system. You cannot accidentally run it early. This example uses regular closures (`FnOnce`) as the synchronous analogy โ same laziness, same capture semantics, no runtime needed.How It Works in Rust
// Create a lazy computation (like: let fut = async { expensive() })
fn lazy_comp<F: FnOnce() -> T, T>(label: &str, f: F) -> impl FnOnce() -> T + '_ {
println!("Creating: {label}"); // runs immediately on creation
move || {
println!("Executing: {label}"); // runs only when called
f()
}
}
// Conditionally run โ like: if cond { fut.await } else { None }
fn run_if<F: FnOnce() -> T, T>(cond: bool, t: F) -> Option<T> {
if cond { Some(t()) } else { None } // the work is skipped entirely if cond is false
}
// Capture by move โ like: async move { x * mult }
let mult = 7i32;
let tasks: Vec<Box<dyn FnOnce() -> i32>> = (1..=5)
.map(|x| -> Box<dyn FnOnce() -> i32> { Box::new(move || x * mult) })
.collect();
The `move` capture transfers `mult` into each closure by value. In async code, `async move { }` does the same thing โ essential when the block needs to outlive the scope that created it.
What This Unlocks
- Conditional execution: Describe work, then decide whether to run it โ skip expensive operations when not needed.
- Dynamic task lists: Build `Vec<Box<dyn Future<Output=T>>>` at runtime and drive them concurrently with `join_all`.
- select! and racing: Works because futures don't start until polled โ you can race two and discard the loser.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Inline async | `fun () -> Lwt.return (f ())` | `async { f() }` |
| Lazy by default | explicit thunks needed | implicit โ not polled until awaited |
| Capture by value | `let x = x in fun () -> x` | `async move { x }` |
| Type of lazy work | `unit -> 'a Lwt.t` | `impl Future<Output = A>` |
//! # Async Blocks and Lazy Evaluation
//!
//! Demonstrates lazy evaluation with closures as a synchronous analogy
//! for async blocks. Work is described but not executed until invoked.
/// Creates a lazy computation that prints a message when created and another when executed.
/// Analogous to `async { }` blocks which describe work without running it.
pub fn lazy_comp<'a, F, T>(label: &'a str, f: F) -> impl FnOnce() -> T + 'a
where
F: FnOnce() -> T + 'a,
{
println!("Creating: {}", label);
move || {
println!("Executing: {}", label);
f()
}
}
/// Conditionally run a lazy computation.
/// Analogous to: `if cond { fut.await } else { None }`
pub fn run_if<F, T>(cond: bool, thunk: F) -> Option<T>
where
F: FnOnce() -> T,
{
if cond {
Some(thunk())
} else {
None
}
}
/// Create multiple lazy tasks that capture a value by move.
/// Analogous to `async move { }` blocks.
pub fn create_tasks_with_capture(multiplier: i32, count: usize) -> Vec<Box<dyn FnOnce() -> i32>> {
(1..=count as i32)
.map(|x| -> Box<dyn FnOnce() -> i32> { Box::new(move || x * multiplier) })
.collect()
}
/// A more idiomatic approach using iterators and Option.
pub fn lazy_filter_map<T, U, F>(items: impl IntoIterator<Item = T>, pred: F) -> Vec<U>
where
F: Fn(&T) -> Option<U>,
{
items.into_iter().filter_map(|x| pred(&x)).collect()
}
/// Chain multiple lazy computations.
pub fn chain_lazy<A, B, C, F, G>(first: F, second: G) -> impl FnOnce() -> C
where
F: FnOnce() -> A,
G: FnOnce(A) -> B,
B: Into<C>,
{
move || second(first()).into()
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn test_lazy_not_called_until_invoked() {
let called = Cell::new(false);
let thunk = || {
called.set(true);
42
};
assert!(!called.get(), "should not be called yet");
let result = thunk();
assert!(called.get(), "should be called now");
assert_eq!(result, 42);
}
#[test]
fn test_run_if_skips_when_false() {
let called = Cell::new(false);
let result = run_if(false, || {
called.set(true);
panic!("should not reach here")
});
assert!(!called.get());
assert!(result.is_none());
}
#[test]
fn test_run_if_executes_when_true() {
let result = run_if(true, || 42);
assert_eq!(result, Some(42));
}
#[test]
fn test_create_tasks_with_capture() {
let tasks = create_tasks_with_capture(7, 5);
assert_eq!(tasks.len(), 5);
let results: Vec<i32> = tasks.into_iter().map(|t| t()).collect();
assert_eq!(results, vec![7, 14, 21, 28, 35]);
}
#[test]
fn test_lazy_filter_map() {
let items = vec![1, 2, 3, 4, 5, 6];
let evens_doubled = lazy_filter_map(items, |&x| {
if x % 2 == 0 {
Some(x * 2)
} else {
None
}
});
assert_eq!(evens_doubled, vec![4, 8, 12]);
}
#[test]
fn test_chain_lazy() {
let computation = chain_lazy::<i32, i32, i32, _, _>(|| 5, |x| x * 2);
assert_eq!(computation(), 10);
}
}
(* OCaml: lazy evaluation with thunks *)
let lazy_comp label f =
Printf.printf "Creating: %s\n" label;
fun () -> Printf.printf "Executing: %s\n" label; f ()
let run_if cond thunk = if cond then Some (thunk ()) else None
let () =
let t1 = lazy_comp "double(5)" (fun () -> 5*2) in
let t2 = lazy_comp "square(4)" (fun () -> 4*4) in
Printf.printf "Result1: %d\n" (t1 ());
Printf.printf "Result2: %d\n" (t2 ());
let r = run_if false (lazy_comp "expensive" (fun () -> 9999)) in
Printf.printf "Cond: %s\n" (match r with None -> "skipped" | Some v -> string_of_int v)