Module sync/once

sync/once
Stability: unstable — the mechanism is settled (double-checked locking over `Mutex.with_lock`, concurrently tested in `tests/sync/once.test.yo`), but one NAME is wrong against the Rust vocabulary the rest of this library follows: `OnceCell(T)` here is thread-safe, which is Rust's `OnceLock<T>`, while Rust's `OnceCell<T>` is the single-threaded one. Renaming it — or adding a genuinely single-threaded cell beside it — is a live question, so the name is not frozen. `LazyLock`'s counterpart is also still missing (`plans/STD_API_STABILIZATION_FINDINGS.md` item 10); that one is additive. Neither type has poisoning: an initializer that exits early leaves the `Once` un-done and the next caller retries. — stable modules only change additively; this one may still change.

One-time initialization primitives.

Once runs a side-effecting closure exactly once; OnceCell(T) stores the value that closure produced and hands it back on every later call.

Example

{ Once, OnceCell } :: import "std/sync/once";

init := Once.new();
// Only the first call executes the function
init.call(() => {
  println("initialized!");
});
init.call(() => {
  println("this will NOT run");
});

cell := OnceCell(i32).new();
a := cell.get_or_init(() => i32(42)); // runs the closure
b := cell.get_or_init(() => i32(99)); // returns 42, closure not run

Stability

unstable — the mechanism is settled (double-checked locking over Mutex.with_lock, concurrently tested in tests/sync/once.test.yo), but one NAME is wrong against the Rust vocabulary the rest of this library follows: OnceCell(T) here is thread-safe, which is Rust's OnceLock<T>, while Rust's OnceCell<T> is the single-threaded one. Renaming it — or adding a genuinely single-threaded cell beside it — is a live question, so the name is not frozen. LazyLock's counterpart is also still missing (plans/STD_API_STABILIZATION_FINDINGS.md item 10); that one is additive. Neither type has poisoning: an initializer that exits early leaves the Once un-done and the next caller retries.

Types

Once atomic object
Once

Execute a function exactly once, thread-safely. Uses atomic reference counting for safe cross-thread sharing.

Fields

NameTypeDescription
_doneatomic_bool
_mutexMutex(bool)
impl(Once, ...)
new : (Once) fn() -> Once

Create an empty cell.

Returns: Once

call : (Once) fn(self : Once, f : Impl : (Fn() -> unit)) -> unit

Run f the first time this is called, and never again — Rust's Once::call_once.

Every later call is a no-op, and a call that arrives WHILE another thread is running f blocks until that f returns, so f has finished for every caller that this returns to. The already-done case costs one Acquire load and takes no lock.

_done is set only after f RETURNS, so an f that exits early leaves the Once un-done and the next caller runs it again. That is the one divergence from Rust, which poisons a Once whose closure panicked and then propagates the panic to every later call_once: Yo has no poisoned state, and marking a half-finished initialization "done" would be worse than retrying it.

f cannot throw — a closure cannot capture an Exception — so there is no "the mutex stayed locked" case to worry about; release is structural through Mutex.with_lock either way.

Parameters

NameTypeNotes
selfOnce
fImpl : (Fn() -> unit)

Returns: unit

is_done : (Once) fn(self : Once) -> bool

Whether the closure has already run to completion — Rust's Once::is_completed.

A lock-free Acquire load, so it is cheap, and it is honest in the direction that matters: true means the initialization is finished and its writes are visible to this thread. false can be stale the instant it is read — another thread may be inside f — so do not use it to decide whether call will block.

Parameters

NameTypeNotes
selfOnce

Returns: bool

OnceCell type-function
fn(T : Type) -> Type

A cell initialized at most once, thread-safely — the value-carrying counterpart of Once (Rust's OnceLock).

Once itself stores nothing but its completion flag, so it cannot hand a value back; OnceCell(T) adds the storage and reuses Once for the double-checked-locking protocol. The Release-store that publishes _done happens after _value is written, and every read goes through an Acquire-load of _done first, so a thread that observes an initialized cell also observes the value the initializer wrote.

Type Parameters

NameTypeNotes
TTypecomptime

Trait Implementations

impl(generic(T : Type), where(T <: (Send, Acyclic)), OnceCell(T), Acyclic())
impl(generic(T : Type), where(T <: (Send, Acyclic)), OnceCell(T), ...)
new : (fn() -> Self)

Create an empty cell.

Returns: Self

get_or_init : (fn(self : Self, f : Impl(Fn() -> T)) -> T)

Return the stored value, initializing it with f if this is the first call. f runs at most once across all threads; every other caller blocks until it finishes and then sees its value.

Returns: T

get : (fn(self : Self) -> Option(T))

Return the stored value, or None if the cell has not been initialized. Never runs an initializer.

Returns: Option(T)

is_initialized : (fn(self : Self) -> bool)

Whether the cell holds a value.

Returns: bool