ExamplesBy LevelBy TopicLearning Paths
435 Fundamental

435: Lazy Static Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "435: Lazy Static Pattern" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Global state initialization in Rust is tricky: `static` variables require `const` initializers, but many useful values (HashMap, compiled regex, config loaded from env) can only be built at runtime. Key difference from OCaml: 1. **Language vs. library**: OCaml's `lazy` is a language keyword; Rust's `OnceLock` is a standard library type.

Tutorial

The Problem

Global state initialization in Rust is tricky: static variables require const initializers, but many useful values (HashMap, compiled regex, config loaded from env) can only be built at runtime. The lazy_static! macro (now largely superseded by std::sync::OnceLock in Rust 1.70+) solves this by wrapping initialization in a once-executed closure. OnceLock<T> provides thread-safe initialization on first access. This pattern enables global singletons, compiled regex caches, and runtime-initialized configuration that is accessed efficiently after the first call.

OnceLock/lazy_static patterns appear in compiled regex caches (the regex crate recommends this), global configuration, connection pool singletons, and any value that is expensive to initialize and needs global access.

🎯 Learning Outcomes

  • • Understand why static mut is unsafe and why OnceLock is the safe alternative
  • • Learn how OnceLock::get_or_init guarantees single initialization across all threads
  • • See how thread_local! provides per-thread storage without synchronization
  • • Understand the historical lazy_static! macro and how OnceLock replaces it
  • • Learn when to use OnceLock (global singleton) vs. Arc<Mutex<T>> (shared mutable state)
  • Code Example

    #![allow(clippy::all)]
    //! Lazy Static Pattern
    //!
    //! Lazy initialization of static values.
    
    use std::sync::OnceLock;
    
    /// Global config using OnceLock.
    static CONFIG: OnceLock<Config> = OnceLock::new();
    
    #[derive(Debug)]
    pub struct Config {
        pub debug: bool,
        pub max_size: usize,
    }
    
    impl Config {
        pub fn global() -> &'static Config {
            CONFIG.get_or_init(|| Config {
                debug: cfg!(debug_assertions),
                max_size: 1024,
            })
        }
    }
    
    /// Thread-local state.
    thread_local! {
        static COUNTER: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
    }
    
    pub fn increment_counter() -> u32 {
        COUNTER.with(|c| {
            let v = c.get() + 1;
            c.set(v);
            v
        })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_config_global() {
            let cfg = Config::global();
            assert_eq!(cfg.max_size, 1024);
        }
    
        #[test]
        fn test_config_same_instance() {
            let cfg1 = Config::global();
            let cfg2 = Config::global();
            assert!(std::ptr::eq(cfg1, cfg2));
        }
    
        #[test]
        fn test_thread_local_counter() {
            let v1 = increment_counter();
            let v2 = increment_counter();
            assert_eq!(v2, v1 + 1);
        }
    
        #[test]
        fn test_config_debug() {
            let cfg = Config::global();
            // In tests, debug_assertions is typically true
            #[cfg(debug_assertions)]
            assert!(cfg.debug);
        }
    
        #[test]
        fn test_multiple_increments() {
            let start = increment_counter();
            increment_counter();
            increment_counter();
            let end = increment_counter();
            assert_eq!(end, start + 3);
        }
    }

    Key Differences

  • Language vs. library: OCaml's lazy is a language keyword; Rust's OnceLock is a standard library type.
  • Thread safety: OnceLock is explicitly designed for concurrent initialization; OCaml's lazy requires explicit locking in OCaml 5.x multi-domain programs.
  • Thread-local: Rust's thread_local! macro uses OS thread-local storage; OCaml 5.x's Domain.DLS provides domain-local storage.
  • Legacy: The lazy_static! crate was widely used before OnceLock; OCaml's Lazy.t has been stable for decades.
  • OCaml Approach

    OCaml uses Lazy.t for lazy values: let config = lazy (make_config ()) where Lazy.force config triggers initialization on first access. Thread safety in OCaml 4.x relies on the GIL; OCaml 5.x's Mutex.t and Atomic.t are needed for true concurrent lazy initialization. Thread_local.t provides per-domain storage in OCaml 5.x. The lazy keyword is built into the OCaml language, unlike Rust's library-based OnceLock.

    Full Source

    #![allow(clippy::all)]
    //! Lazy Static Pattern
    //!
    //! Lazy initialization of static values.
    
    use std::sync::OnceLock;
    
    /// Global config using OnceLock.
    static CONFIG: OnceLock<Config> = OnceLock::new();
    
    #[derive(Debug)]
    pub struct Config {
        pub debug: bool,
        pub max_size: usize,
    }
    
    impl Config {
        pub fn global() -> &'static Config {
            CONFIG.get_or_init(|| Config {
                debug: cfg!(debug_assertions),
                max_size: 1024,
            })
        }
    }
    
    /// Thread-local state.
    thread_local! {
        static COUNTER: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
    }
    
    pub fn increment_counter() -> u32 {
        COUNTER.with(|c| {
            let v = c.get() + 1;
            c.set(v);
            v
        })
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_config_global() {
            let cfg = Config::global();
            assert_eq!(cfg.max_size, 1024);
        }
    
        #[test]
        fn test_config_same_instance() {
            let cfg1 = Config::global();
            let cfg2 = Config::global();
            assert!(std::ptr::eq(cfg1, cfg2));
        }
    
        #[test]
        fn test_thread_local_counter() {
            let v1 = increment_counter();
            let v2 = increment_counter();
            assert_eq!(v2, v1 + 1);
        }
    
        #[test]
        fn test_config_debug() {
            let cfg = Config::global();
            // In tests, debug_assertions is typically true
            #[cfg(debug_assertions)]
            assert!(cfg.debug);
        }
    
        #[test]
        fn test_multiple_increments() {
            let start = increment_counter();
            increment_counter();
            increment_counter();
            let end = increment_counter();
            assert_eq!(end, start + 3);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_config_global() {
            let cfg = Config::global();
            assert_eq!(cfg.max_size, 1024);
        }
    
        #[test]
        fn test_config_same_instance() {
            let cfg1 = Config::global();
            let cfg2 = Config::global();
            assert!(std::ptr::eq(cfg1, cfg2));
        }
    
        #[test]
        fn test_thread_local_counter() {
            let v1 = increment_counter();
            let v2 = increment_counter();
            assert_eq!(v2, v1 + 1);
        }
    
        #[test]
        fn test_config_debug() {
            let cfg = Config::global();
            // In tests, debug_assertions is typically true
            #[cfg(debug_assertions)]
            assert!(cfg.debug);
        }
    
        #[test]
        fn test_multiple_increments() {
            let start = increment_counter();
            increment_counter();
            increment_counter();
            let end = increment_counter();
            assert_eq!(end, start + 3);
        }
    }

    Deep Comparison

    OCaml vs Rust: macro lazy static

    See example.rs and example.ml for side-by-side implementations.

    Key Points

  • Rust macros operate at compile time
  • OCaml uses ppx for similar metaprogramming
  • Both languages support powerful code generation
  • Rust's macro_rules! is built into the language
  • OCaml's approach requires external tooling
  • Exercises

  • Compiled regex cache: Use OnceLock<Regex> to cache a compiled regex pattern. Implement fn is_valid_email(s: &str) -> bool that initializes the regex once and reuses it. Verify with a test that the regex is only compiled once.
  • Config from env: Create AppConfig::global() using OnceLock that reads configuration from environment variables on first call. Include DATABASE_URL, PORT, and LOG_LEVEL. Use once_cell::sync::Lazy or OnceLock to make the initialization thread-safe.
  • Per-thread ID: Use thread_local! to assign each thread a unique ID on first access. Spawn 4 threads and verify each has a unique ID by collecting thread IDs from all threads and asserting they are all distinct.
  • Open Source Repos