🦀 Functional Rust

456: OnceLock — Initialize a Global Exactly Once

Difficulty: 3 Level: Intermediate Use `OnceLock<T>` for thread-safe lazy initialization of global state — the safe replacement for `static mut` and the `lazy_static` crate.

The Problem This Solves

Global state is often necessary: database connection strings, compiled regexes, configuration loaded from environment variables, expensive lookup tables. But initializing them at the `static` declaration (`static CONFIG: Config = init()`) requires a `const` expression — no runtime computation, no `std::env::var`, no file I/O. The common workaround in unsafe languages is `static mut` with manual initialization: check a flag, if unset, initialize, set the flag. This has an obvious race condition if multiple threads check simultaneously. The traditional Rust fix was the `lazy_static` crate (or its successor `once_cell`). Since Rust 1.70, `OnceLock<T>` is in the standard library and covers the thread-safe case directly. `OnceLock` guarantees that the initializer closure runs exactly once across all threads, even if multiple threads call `get_or_init` simultaneously. The first thread to arrive runs the closure; others wait and then get the same initialized value. After initialization, reads are lock-free — just a check of an internal flag.

The Intuition

`OnceLock<T>` is a container that transitions from "empty" to "filled" exactly once. Before filling: reads return `None`; `get_or_init` runs the closure. After filling: reads return `Some(&T)` directly, no locking needed. In Java: `static` fields with class-loading guarantees (complicated) or double-checked locking (error-prone). In Python: module-level initialization runs once (but has subtleties with threads). In Go: `sync.Once`. In Rust, `OnceLock` is the standard answer — explicit, composable, and correct without understanding JVM class loading rules.

How It Works in Rust

use std::sync::OnceLock;
use std::collections::HashMap;

// Global static — OnceLock is the type-safe container
static CONFIG: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();

// Function that initializes once and returns reference forever after
fn config() -> &'static HashMap<&'static str, &'static str> {
 CONFIG.get_or_init(|| {
     println!("init config");  // runs exactly once, even with 100 threads
     [("host", "localhost"), ("port", "8080")]
         .iter().cloned().collect()
 })
}

fn main() {
 // All three calls return the same reference — init runs once
 println!("{}", config()["host"]);  // "init config" printed here
 println!("{}", config()["host"]);  // no print — already initialized
 assert!(std::ptr::eq(config(), config())); // literally the same pointer

 // Explicit set (instead of get_or_init)
 static GREETING: OnceLock<String> = OnceLock::new();
 GREETING.set("Hello!".to_string()).ok(); // Ok(()) or Err(value) if already set
 println!("{}", GREETING.get().unwrap());
}
For non-global (per-instance) lazy initialization, use `OnceCell<T>` from `std::cell` — same semantics but single-threaded (no `Sync`). This is useful for caching a computed field on a struct.
use std::cell::OnceCell;

struct Expensive { cache: OnceCell<Vec<u32>>, limit: u32 }
impl Expensive {
 fn computed(&self) -> &[u32] {
     self.cache.get_or_init(|| {
         (2..=self.limit).filter(|&n| (2..n).all(|d| n % d != 0)).collect()
     })
 }
}

What This Unlocks

Key Differences

ConceptOCamlRust
Lazy value`lazy (expensive ())``OnceLock::new()` with `get_or_init(...)`
Force evaluation`Lazy.force v``lock.get_or_init(\\compute())`
Thread-safe`Lazy.t` safe in OCaml 5`OnceLock<T>` — `SeqCst` guarantees, single init
Per-instancesame `Lazy.t``OnceCell<T>` from `std::cell` — single-threaded
Static global`let v = lazy (...)``static V: OnceLock<T> = OnceLock::new()`
ReplacesN/A`static mut`, `lazy_static` crate, `once_cell` crate
// 456. OnceLock and OnceCell for lazy init
use std::sync::OnceLock;
use std::cell::OnceCell;
use std::collections::HashMap;

static CONFIG: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
static GREETING: OnceLock<String> = OnceLock::new();

fn config() -> &'static HashMap<&'static str, &'static str> {
    CONFIG.get_or_init(|| {
        println!("init config");
        [("host","localhost"),("port","8080")].iter().cloned().collect()
    })
}

fn main() {
    // Called 3 times — init runs once
    println!("host={}", config()["host"]);
    println!("host={}", config()["host"]);
    println!("Same ptr: {}", std::ptr::eq(config(), config()));

    // Set explicitly
    GREETING.set("Hello, world!".to_string()).ok();
    println!("{}", GREETING.get().unwrap());

    // OnceCell — per-instance, not global
    struct Lazy { cache: OnceCell<Vec<u32>>, limit: u32 }
    impl Lazy {
        fn primes(&self) -> &[u32] {
            self.cache.get_or_init(|| {
                (2..=self.limit).filter(|&n| (2..n).all(|d| n%d!=0)).collect()
            })
        }
    }
    let l = Lazy { cache: OnceCell::new(), limit: 30 };
    println!("primes: {:?}", l.primes());
    println!("cached: {:?}", l.primes()); // same slice
}

#[cfg(test)]
mod tests {
    use std::sync::OnceLock;
    use std::sync::atomic::{AtomicU32,Ordering};
    #[test] fn test_once_only() {
        let lock: OnceLock<u32> = OnceLock::new();
        let n = AtomicU32::new(0);
        lock.get_or_init(|| { n.fetch_add(1,Ordering::SeqCst); 42 });
        lock.get_or_init(|| { n.fetch_add(1,Ordering::SeqCst); 99 });
        assert_eq!(*lock.get().unwrap(), 42);
        assert_eq!(n.load(Ordering::SeqCst), 1);
    }
}
(* 456. Lazy init – OCaml *)
let config = lazy (
  Printf.printf "init config\n%!";
  [("host","localhost");("port","8080")])

let primes_under_50 = lazy (
  Printf.printf "computing primes\n%!";
  let s = Array.make 51 true in
  s.(0)<-false; s.(1)<-false;
  for i=2 to 7 do if s.(i) then
    let j=ref(i*i) in while !j<=50 do s.(!j)<-false; j:= !j+i done
  done;
  Array.to_list (Array.init 51 (fun i -> if s.(i) then [i] else []) |> Array.concat)
)

let () =
  let v1 = Lazy.force config in
  let v2 = Lazy.force config in  (* not recomputed *)
  assert (v1 == v2);             (* same object *)
  Printf.printf "host=%s\n" (List.assoc "host" v1);
  Printf.printf "primes: %s\n"
    (String.concat "," (List.map string_of_int (Lazy.force primes_under_50)))