ExamplesBy LevelBy TopicLearning Paths
515 Intermediate

Lazy Evaluation with OnceLock

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "Lazy Evaluation with OnceLock" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Lazy initialization solves a fundamental tension: some values are expensive to compute but not always needed, and global mutable state is unsafe in concurrent programs. Key difference from OCaml: 1. **Language integration**: OCaml has `lazy`/`Lazy.force` as first

Tutorial

The Problem

Lazy initialization solves a fundamental tension: some values are expensive to compute but not always needed, and global mutable state is unsafe in concurrent programs. Before OnceLock, Rust programs used unsafe code or external crates like lazy_static for program-global lazy values. std::sync::OnceLock (stabilized in Rust 1.70) provides a safe, lock-free, thread-safe cell initialized exactly once. This pattern is ubiquitous in database connection pools, configuration parsers, and compiled regex caches.

🎯 Learning Outcomes

  • • How OnceLock<T> guarantees initialization happens exactly once, even under concurrent access
  • • Why lazy initialization avoids paying the cost of computation when a value is never used
  • • How to build structs with lazily computed derived fields using OnceLock members
  • • The difference between OnceLock (runtime init) and const/static (compile-time init)
  • • How the Lazy<T, F> pattern encapsulates initialization logic alongside the value
  • Code Example

    use std::sync::OnceLock;
    
    static EXPENSIVE: OnceLock<i64> = OnceLock::new();
    
    fn get_value() -> i64 {
        *EXPENSIVE.get_or_init(|| (1..=1000i64).sum())
    }

    Key Differences

  • Language integration: OCaml has lazy/Lazy.force as first-class syntax and stdlib types; Rust uses std::sync::OnceLock as a library type without special syntax.
  • Thread safety model: OnceLock uses atomic operations for lock-free initialization; OCaml's Lazy.t in 5.x uses a per-cell mutex, simpler but with more overhead.
  • Failure handling: OCaml's Lazy.force can raise exceptions from the initializer; Rust's get_or_init panics if the initializer panics (poison), and get_or_try_init returns Result.
  • Struct fields: Rust can have multiple independent OnceLock fields in one struct; OCaml wraps individual Lazy.t values in records with the same ergonomics.
  • OCaml Approach

    OCaml uses Lazy.t — a built-in type for deferred computation. lazy expr creates a thunk; Lazy.force evaluates it on first call and memoizes the result. OCaml's garbage collector handles the memory, and the runtime ensures thread safety via a mutex per lazy cell in OCaml 5.x.

    let expensive = lazy ((List.fold_left (+) 0 (List.init 1000 (fun i -> i + 1))))
    let value = Lazy.force expensive  (* computed once *)
    

    Full Source

    #![allow(clippy::all)]
    //! Lazy Evaluation with OnceLock
    //!
    //! Deferred computation using std::sync::OnceLock.
    
    use std::sync::OnceLock;
    
    /// Global lazy value — initialized once on first access.
    static EXPENSIVE_VALUE: OnceLock<i64> = OnceLock::new();
    
    pub fn get_expensive_value() -> i64 {
        *EXPENSIVE_VALUE.get_or_init(|| (1..=1_000i64).sum())
    }
    
    /// Lazy struct: computes fields only when accessed.
    pub struct LazyConfig {
        raw: String,
        parsed_items: OnceLock<Vec<String>>,
        item_count: OnceLock<usize>,
    }
    
    impl LazyConfig {
        pub fn new(raw: &str) -> Self {
            LazyConfig {
                raw: raw.to_string(),
                parsed_items: OnceLock::new(),
                item_count: OnceLock::new(),
            }
        }
    
        pub fn items(&self) -> &[String] {
            self.parsed_items.get_or_init(|| {
                self.raw
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect()
            })
        }
    
        pub fn count(&self) -> usize {
            *self.item_count.get_or_init(|| self.items().len())
        }
    
        pub fn raw(&self) -> &str {
            &self.raw
        }
    }
    
    /// Lazy computation with custom initializer.
    pub struct Lazy<T, F = fn() -> T> {
        cell: OnceLock<T>,
        init: F,
    }
    
    impl<T, F: Fn() -> T> Lazy<T, F> {
        pub const fn new(init: F) -> Self {
            Lazy {
                cell: OnceLock::new(),
                init,
            }
        }
    
        pub fn get(&self) -> &T {
            self.cell.get_or_init(&self.init)
        }
    }
    
    /// Memoized single-value computation.
    pub struct Memo<T> {
        value: OnceLock<T>,
    }
    
    impl<T> Memo<T> {
        pub const fn new() -> Self {
            Memo {
                value: OnceLock::new(),
            }
        }
    
        pub fn get_or_compute(&self, compute: impl FnOnce() -> T) -> &T {
            self.value.get_or_init(compute)
        }
    }
    
    impl<T> Default for Memo<T> {
        fn default() -> Self {
            Self::new()
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::sync::atomic::{AtomicUsize, Ordering};
    
        #[test]
        fn test_expensive_value() {
            let v1 = get_expensive_value();
            let v2 = get_expensive_value();
            assert_eq!(v1, v2);
            assert_eq!(v1, 500500); // sum of 1..=1000
        }
    
        #[test]
        fn test_lazy_config_items() {
            let cfg = LazyConfig::new("a, b, c, d");
            assert_eq!(cfg.items(), &["a", "b", "c", "d"]);
        }
    
        #[test]
        fn test_lazy_config_count() {
            let cfg = LazyConfig::new("x, y, z");
            assert_eq!(cfg.count(), 3);
        }
    
        #[test]
        fn test_lazy_config_caches() {
            let cfg = LazyConfig::new("one, two");
            let items1 = cfg.items();
            let items2 = cfg.items();
            // Same reference (cached)
            assert!(std::ptr::eq(items1, items2));
        }
    
        #[test]
        fn test_memo_computes_once() {
            static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
            let memo: Memo<i32> = Memo::new();
    
            let v1 = memo.get_or_compute(|| {
                CALL_COUNT.fetch_add(1, Ordering::SeqCst);
                42
            });
    
            let v2 = memo.get_or_compute(|| {
                CALL_COUNT.fetch_add(1, Ordering::SeqCst);
                99
            });
    
            assert_eq!(*v1, 42);
            assert_eq!(*v2, 42);
            assert_eq!(CALL_COUNT.load(Ordering::SeqCst), 1);
        }
    
        #[test]
        fn test_lazy_struct() {
            static INIT_COUNT: AtomicUsize = AtomicUsize::new(0);
    
            let lazy = Lazy::new(|| {
                INIT_COUNT.fetch_add(1, Ordering::SeqCst);
                vec![1, 2, 3]
            });
    
            assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 0);
            assert_eq!(lazy.get(), &vec![1, 2, 3]);
            assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 1);
            assert_eq!(lazy.get(), &vec![1, 2, 3]);
            assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 1);
        }
    
        #[test]
        fn test_empty_config() {
            let cfg = LazyConfig::new("");
            assert!(cfg.items().is_empty());
            assert_eq!(cfg.count(), 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        use std::sync::atomic::{AtomicUsize, Ordering};
    
        #[test]
        fn test_expensive_value() {
            let v1 = get_expensive_value();
            let v2 = get_expensive_value();
            assert_eq!(v1, v2);
            assert_eq!(v1, 500500); // sum of 1..=1000
        }
    
        #[test]
        fn test_lazy_config_items() {
            let cfg = LazyConfig::new("a, b, c, d");
            assert_eq!(cfg.items(), &["a", "b", "c", "d"]);
        }
    
        #[test]
        fn test_lazy_config_count() {
            let cfg = LazyConfig::new("x, y, z");
            assert_eq!(cfg.count(), 3);
        }
    
        #[test]
        fn test_lazy_config_caches() {
            let cfg = LazyConfig::new("one, two");
            let items1 = cfg.items();
            let items2 = cfg.items();
            // Same reference (cached)
            assert!(std::ptr::eq(items1, items2));
        }
    
        #[test]
        fn test_memo_computes_once() {
            static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
            let memo: Memo<i32> = Memo::new();
    
            let v1 = memo.get_or_compute(|| {
                CALL_COUNT.fetch_add(1, Ordering::SeqCst);
                42
            });
    
            let v2 = memo.get_or_compute(|| {
                CALL_COUNT.fetch_add(1, Ordering::SeqCst);
                99
            });
    
            assert_eq!(*v1, 42);
            assert_eq!(*v2, 42);
            assert_eq!(CALL_COUNT.load(Ordering::SeqCst), 1);
        }
    
        #[test]
        fn test_lazy_struct() {
            static INIT_COUNT: AtomicUsize = AtomicUsize::new(0);
    
            let lazy = Lazy::new(|| {
                INIT_COUNT.fetch_add(1, Ordering::SeqCst);
                vec![1, 2, 3]
            });
    
            assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 0);
            assert_eq!(lazy.get(), &vec![1, 2, 3]);
            assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 1);
            assert_eq!(lazy.get(), &vec![1, 2, 3]);
            assert_eq!(INIT_COUNT.load(Ordering::SeqCst), 1);
        }
    
        #[test]
        fn test_empty_config() {
            let cfg = LazyConfig::new("");
            assert!(cfg.items().is_empty());
            assert_eq!(cfg.count(), 0);
        }
    }

    Deep Comparison

    OCaml vs Rust: Lazy Evaluation

    OCaml

    (* Built-in lazy keyword *)
    let expensive = lazy (List.fold_left (+) 0 (List.init 1000 Fun.id))
    
    let value = Lazy.force expensive  (* computed on first force *)
    let value2 = Lazy.force expensive (* cached *)
    

    Rust

    use std::sync::OnceLock;
    
    static EXPENSIVE: OnceLock<i64> = OnceLock::new();
    
    fn get_value() -> i64 {
        *EXPENSIVE.get_or_init(|| (1..=1000i64).sum())
    }
    

    Key Differences

  • OCaml: Built-in lazy keyword and Lazy.force
  • Rust: Uses OnceLock (thread-safe) or OnceCell (single-thread)
  • OCaml: Lazy values are first-class with explicit forcing
  • Rust: Closures passed to get_or_init for deferred computation
  • Both ensure computation happens at most once
  • Exercises

  • Lazy regex cache: Build a struct RegexCache with a OnceLock<Vec<String>> field that lazily compiles a list of patterns from a raw comma-separated string on first access.
  • Cached factorial: Implement a FactorialCache that computes and caches n! for n up to 20 using an array of OnceLock<u64>, ensuring each entry is computed only once.
  • Initialization error: Modify Lazy<T, F> to use OnceLock<Result<T, String>> so initialization failures are stored and returned on every subsequent access instead of panicking.
  • Open Source Repos