๐Ÿฆ€ Functional Rust

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:

What This Unlocks

Key Differences

ConceptOCamlRust
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 safetyNot 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"