435: Lazy Static Pattern
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
static mut is unsafe and why OnceLock is the safe alternativeOnceLock::get_or_init guarantees single initialization across all threadsthread_local! provides per-thread storage without synchronizationlazy_static! macro and how OnceLock replaces itOnceLock (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
lazy is a language keyword; Rust's OnceLock is a standard library type.OnceLock is explicitly designed for concurrent initialization; OCaml's lazy requires explicit locking in OCaml 5.x multi-domain programs.thread_local! macro uses OS thread-local storage; OCaml 5.x's Domain.DLS provides domain-local storage.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);
}
}#[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
Exercises
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.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.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.