323: async blocks and Lazy Evaluation
Tutorial Video
Text description (accessibility)
This video demonstrates the "323: async blocks and Lazy Evaluation" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. `async fn` creates a function that returns a `Future`. Key difference from OCaml: 1. **Explicit laziness**: Rust async blocks are explicitly lazy — nothing runs until `.await`; OCaml's `Lwt.t` values trigger computation when bound.
Tutorial
The Problem
async fn creates a function that returns a Future. async { } blocks create anonymous futures inline. The key property of both is laziness: the code inside does not execute until the future is .awaited or driven by an executor. This is fundamentally different from eager evaluation — a value computed immediately. Understanding this laziness is essential for avoiding bugs where code runs at the wrong time or doesn't run at all.
🎯 Learning Outcomes
async { } blocks create futures that are lazy — code doesn't run until .awaitedasync move to capture values by ownership into an async block.await a future causes silent bugs (the computation never runs)Code Example
fn lazy_comp<F: FnOnce() -> T, T>(label: &str, f: F) -> impl FnOnce() -> T + '_ {
println!("Creating: {label}");
move || { println!("Executing: {label}"); f() }
}Key Differences
.await; OCaml's Lwt.t values trigger computation when bound.async move { } captures environment by value; regular async { } may borrow — ownership rules apply to async blocks..await-ing a future silently discards it; in OCaml, not binding a Lwt.t has similar silent effects.async { } block has type impl Future<Output = T> where T is the last expression's type.OCaml Approach
OCaml's Lwt promises are also lazy in a sense — a Lwt.t value represents a pending computation, and only resolving it (via Lwt.bind or >>=) triggers continuation:
(* Thunk: lazy value in OCaml *)
let lazy_comp label f =
Printf.printf "Creating: %s\n" label;
fun () ->
Printf.printf "Executing: %s\n" label;
f ()
Full Source
#![allow(clippy::all)]
//! # 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);
}
}#[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);
}
}
Deep Comparison
OCaml vs Rust: Async Blocks and Lazy Evaluation
Lazy Computation Creation
OCaml:
let lazy_comp label f =
Printf.printf "Creating: %s\n" label;
fun () -> Printf.printf "Executing: %s\n" label; f ()
Rust:
fn lazy_comp<F: FnOnce() -> T, T>(label: &str, f: F) -> impl FnOnce() -> T + '_ {
println!("Creating: {label}");
move || { println!("Executing: {label}"); f() }
}
Conditional Execution
OCaml:
let run_if cond thunk = if cond then Some (thunk ()) else None
Rust:
fn run_if<F: FnOnce() -> T, T>(cond: bool, t: F) -> Option<T> {
if cond { Some(t()) } else { None }
}
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Thunk type | unit -> 'a | impl FnOnce() -> T |
| Closure syntax | fun () -> ... | \|\| { ... } |
| Move semantics | Implicit (GC) | Explicit move keyword |
| Type constraints | Inferred | Explicit trait bounds |
| Laziness | Explicit thunks | Implicit in async, explicit here |
Exercises
run_if(cond: bool, thunk: impl FnOnce() -> T) -> Option<T> and show its analogy to if cond { Some(fut.await) } else { None }.lazy_comp closure that captures a String by move can only be called once (FnOnce semantics).