435: lazy_static! / OnceLock Pattern
Difficulty: 3 Level: Advanced Initialise a global value exactly once on first access and reuse it safely across threads โ the modern way with `OnceLock` / `LazyLock`, and what the old `lazy_static!` macro was doing underneath.The Problem This Solves
Global constants in Rust must be computable at compile time. That rules out anything that requires heap allocation (`Vec`, `HashMap`, `String`), system calls, or complex computation. Yet programs often need process-wide singletons: a compiled regex, a connection pool, a config map loaded from environment variables, a prime sieve. The two wrong approaches: compute it every call (wasteful), or initialise it in `main` and thread it through every function as a parameter (ergonomic nightmare). What you want is a global that initialises itself the first time it's needed and is then shared โ zero cost on subsequent accesses, thread-safe, no manual synchronisation. `OnceLock<T>` (stable since Rust 1.70) and `LazyLock<T>` (stable since Rust 1.80) are the standard library answer. `lazy_static!` from the eponymous crate was the community solution before these were stabilised; understanding `OnceLock` demystifies what that macro was generating.The Intuition
`OnceLock<T>` is a cell that transitions from "empty" to "full" exactly once. The first caller of `get_or_init(|| ...)` runs the closure and stores the result; all subsequent callers get a reference to the stored value. The transition is atomic โ safe across multiple threads racing to initialise the same global. `LazyLock<T>` wraps this into a static that evaluates its initialiser on first deref, so you don't even need a wrapper function. The old `lazy_static!` macro generated essentially the same structure: a static `OnceLock`-like wrapper, a function to get the inner reference, and a `Deref` impl to make it transparent. Now the standard library provides this directly.How It Works in Rust
use std::sync::{OnceLock, Mutex};
use std::collections::HashMap;
// โโ OnceLock: initialise on first call, reuse forever โโโโโโโโโโโโโโโโโโโโโโโโ
static GLOBAL_CONFIG: OnceLock<HashMap<String, String>> = OnceLock::new();
fn get_config() -> &'static HashMap<String, String> {
GLOBAL_CONFIG.get_or_init(|| {
println!("Initializing config (runs ONCE)...");
let mut m = HashMap::new();
m.insert("host".to_string(), "localhost".to_string());
m.insert("port".to_string(), "8080".to_string());
m
})
}
// Second call: closure doesn't run; returns cached reference immediately.
// โโ Thread-safe mutable singleton โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
static COUNTER: OnceLock<Mutex<u64>> = OnceLock::new();
fn increment() -> u64 {
let mut c = COUNTER.get_or_init(|| Mutex::new(0)).lock().unwrap();
*c += 1;
*c
}
// โโ LazyLock (Rust 1.80+) โ closure in the static itself โโโโโโโโโโโโโโโโโโโโโ
// use std::sync::LazyLock;
// static PRIMES: LazyLock<Vec<u32>> = LazyLock::new(|| sieve(100));
// Access: &*PRIMES or just PRIMES[i] (Deref)
// โโ What lazy_static! was generating (simplified) โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// macro_rules! lazy_static_sim {
// (static ref $name:ident : $ty:ty = $init:expr ;) => {
// static $name: OnceLock<$ty> = OnceLock::new();
// fn get() -> &'static $ty { $name.get_or_init(|| $init) }
// };
// }
When to use which:
- `OnceLock<T>` โ when you want explicit control over when initialisation happens, or need `set()` separately from `get_or_init()`
- `LazyLock<T>` โ when you want the simplest possible "global that initialises itself"
- `lazy_static!` crate โ for pre-1.80 compatibility, or in ecosystems that prefer explicit crate usage
What This Unlocks
- Compiled regexes as globals โ `static RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\d+").unwrap())` โ compiled once, used everywhere, no per-call overhead.
- Config loaded at startup โ read environment variables and build a config map once; all code gets a `&'static Config` with no Arc or RefCell.
- Computed lookup tables โ sieve of Eratosthenes, trigonometric tables, hash seeds โ initialised once, accessible from any thread.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Global mutable state | `ref` values at module level; not thread-safe by default | `OnceLock<Mutex<T>>` โ thread-safe, initialised once |
| Lazy initialisation | `lazy_t` (3rd party); or `let x = lazy (fun () -> ...)` | `OnceLock::get_or_init` / `LazyLock::new` (std) |
| Thread safety | Not guaranteed; explicit locking | `OnceLock` is `Sync`; initialisation is atomic |
| Equivalent of `lazy_static!` | No standard equivalent | `LazyLock<T>` (Rust 1.80+) |
// lazy_static! / once_cell pattern in Rust
use std::sync::{OnceLock, Mutex};
use std::collections::HashMap;
// Modern std approach: LazyLock (stable since Rust 1.80)
// OnceLock is stable since 1.70
// Static with lazy initialization using OnceLock
static GLOBAL_CONFIG: OnceLock<HashMap<String, String>> = OnceLock::new();
fn get_config() -> &'static HashMap<String, String> {
GLOBAL_CONFIG.get_or_init(|| {
println!("Initializing config (once)...");
let mut m = HashMap::new();
m.insert("host".to_string(), "localhost".to_string());
m.insert("port".to_string(), "8080".to_string());
m.insert("debug".to_string(), "false".to_string());
m
})
}
// Thread-safe singleton counter
static COUNTER: OnceLock<Mutex<u64>> = OnceLock::new();
fn get_counter() -> &'static Mutex<u64> {
COUNTER.get_or_init(|| Mutex::new(0))
}
fn increment() -> u64 {
let mut c = get_counter().lock().unwrap();
*c += 1;
*c
}
// Simulate lazy_static! macro (before OnceLock was stable)
macro_rules! lazy_static_sim {
(static ref $name:ident : $ty:ty = $init:expr ;) => {
static $name: OnceLock<$ty> = OnceLock::new();
fn get_lazy_() -> &'static $ty {
$name.get_or_init(|| $init)
}
};
}
// Using LazyLock (Rust 1.80+)
// use std::sync::LazyLock;
// static PRIMES: LazyLock<Vec<u32>> = LazyLock::new(|| {
// println!("Computing primes...");
// sieve_of_eratosthenes(100)
// });
fn sieve(limit: usize) -> Vec<u32> {
let mut is_prime = vec![true; limit + 1];
is_prime[0] = false;
if limit > 0 { is_prime[1] = false; }
let mut i = 2;
while i * i <= limit {
if is_prime[i] {
let mut j = i * i;
while j <= limit { is_prime[j] = false; j += i; }
}
i += 1;
}
(2..=limit).filter(|&n| is_prime[n]).map(|n| n as u32).collect()
}
static PRIMES_100: OnceLock<Vec<u32>> = OnceLock::new();
fn get_primes() -> &'static [u32] {
PRIMES_100.get_or_init(|| {
println!("Computing primes up to 100...");
sieve(100)
})
}
fn main() {
// Config initialized once
let config = get_config();
println!("host: {}", config["host"]);
println!("port: {}", config["port"]);
// Second access โ no re-initialization
let config2 = get_config();
println!("host again: {}", config2["host"]);
// Thread-safe counter
println!("
Counter: {}", increment());
println!("Counter: {}", increment());
println!("Counter: {}", increment());
// Primes (lazy)
println!("
Primes <= 100 (first access):");
let primes = get_primes();
println!("{:?}", &primes[..10]);
println!("Primes again (cached):");
println!("{} primes total", get_primes().len());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config() {
let c = get_config();
assert_eq!(c["host"], "localhost");
}
#[test]
fn test_primes() {
let p = get_primes();
assert_eq!(p[0], 2);
assert_eq!(p[1], 3);
assert!(p.contains(&97)); // 97 is prime
}
#[test]
fn test_counter() {
let a = increment();
let b = increment();
assert!(b > a);
}
}
(* lazy_static / once_cell patterns in OCaml *)
(* OCaml module-level lets are eager, not lazy *)
(* For lazy: use lazy keyword or ref *)
let lazy_value = lazy (
Printf.printf "Initializing expensive value...\n";
let result = List.fold_left (+) 0 (List.init 1000 (fun i -> i)) in
result
)
(* Global config (eager in OCaml) *)
let global_config = Hashtbl.create 16
let () =
Hashtbl.add global_config "host" "localhost";
Hashtbl.add global_config "port" "8080"
(* Singleton via module *)
module Registry = struct
let instance : (string, string) Hashtbl.t = Hashtbl.create 16
let register key value = Hashtbl.replace instance key value
let lookup key = Hashtbl.find_opt instance key
end
let () =
Registry.register "version" "1.0.0";
let v = Lazy.force lazy_value in
Printf.printf "Lazy value: %d\n" v;
Printf.printf "Again (cached): %d\n" (Lazy.force lazy_value);
match Registry.lookup "version" with
| Some v -> Printf.printf "Version: %s\n" v
| None -> Printf.printf "Not found\n"