Module sync/refcell

sync/refcell
Stability: unstable — may still change; see below.

RefCell(T): shared mutation checked at run time, on one thread.

The one spelling of the dynamic exclusivity check (plans/VALUES_BY_DEFAULT.md decisions 41 and 46). An Rc(T)'s payload is read-only through the handle, as in Rust; a payload that several handles mutate is an Rc(RefCell(T)). Every access is scoped to a closure, as Mutex(T)'s is: with lends the value &T, with_mut lends it &mut T, and a conflicting access while one is running panics, as Rust's RefCell::borrow_mut does. Mutex blocks; RefCell panics.

{ RefCell } :: import("std/sync/refcell");
log := rc(RefCell(i32).new(i32(0)));
other := log; // a second handle to the same cell
other.replace_with(n => (n + i32(1))); // returns the old value, 0
seen := log.with(n => n); // 1

Beside the two closure doors, the cell has Rust's value-moving methods, which lend nothing to the caller: replace(v) and take() swap a value in and return the old one, replace_with(f) computes the new value from the current one, and into_inner() consumes the cell.

Stability

unstable — Generation A of decision 46. The expression-scoped projections get() / get_mut() (cell.get_mut().field = x) wait for the compiler support that releases a mark at the end of the enclosing expression and rejects binding the projection (plans/VALUES_BY_DEFAULT.md §6 "Decision 46"). The value lives in a cell of its own (one allocation) until a &T receiver is passed by address (decision 43).

Types

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

A value that is mutated through shared handles on one thread, with the exclusivity of each access checked at run time — Rust's RefCell<T>, without a guard: access is granted only inside a closure.

  • with(body) runs body with v : &T; several may run at once (nested), and none while a with_mut body runs.
  • with_mut(body) runs body with v : &mut T, alone: a with or with_mut on the same cell while it runs, or a with_mut while a with runs, panics.
  • replace(v), replace_with(f) and take() store a new value and return the old one, lending nothing to the caller; each panics while a with or with_mut body runs, and replace_with holds the cell as with_mut does while f runs. into_inner() consumes the cell.

The mark is released when the body returns or unwinds. A RefCell is Send when T is and never Sync, so Arc(RefCell(T)) is rejected: across threads, share an Arc(Mutex(T)) or Arc(RwLock(T)).

Type Parameters

NameTypeNotes
TTypecomptime

Trait Implementations

impl(generic(T : Type), RefCell(T), ...)
new : (fn(value : T) -> Self)

A new cell holding value, with no access running.

Returns: Self

with : (fn(generic(R : Type), self : &Self, body : &Impl(Fn(v : &T) -> R)) -> R)

Run body with the value lent read-only, and return its result — Rust's borrow() with the guard's scope. Panics if a with_mut body of this cell is running.

Returns: R

with_mut : (fn(generic(R : Type), self : &Self, body : &Impl(Fn(v : &mut T) -> R)) -> R)

Run body with the value lent exclusively, and return its result — Rust's borrow_mut() with the guard's scope. Panics if any with or with_mut body of this cell is running.

Returns: R

replace : (fn(self : &Self, value : T) -> T)

Store value and return the value it replaces — Rust's replace. Panics if any with or with_mut body of this cell is running: the old value moves out, and a running body is reading or writing it.

Returns: T

replace_with : (fn(self : &Self, f : &Impl(Fn(v : &T) -> T)) -> T)

Store f(v), computed from the current value v, and return the value it replaces — Rust's replace_with. f reads the value as v : &T, as a with body does, and returns its successor: the result replaces the value wholesale, so f is not lent &mut T (an edit in place is with_mut). The cell is held as with_mut holds it while f runs, so any access to it from f panics; the mark is released when f returns or unwinds, and an unwinding f leaves the value unchanged. Panics if any with or with_mut body of this cell is running.

Unlike Rust's replace_with, whose f is an FnOnce(&mut T) -> T, f is an Fn lent &T, as a with body is: it cannot move a captured move-only value into its result (E0913). Until V2b gives a String or a collection its own buffer, an f that returns its argument unchanged (v => v) returns a value sharing storage with the old one, so a write through either reaches the other; return v.clone() for an independent value.

{ RefCell } :: import("std/sync/refcell");
hits := RefCell(i32).new(i32(4));
old := hits.replace_with(n => (n + i32(1))); // old is 4, the cell holds 5

Returns: T

into_inner : (fn(self : Self) -> T)

The value, out of the consumed cell — Rust's into_inner. No mark is checked: a running with or with_mut body holds the cell borrowed, so the cell cannot be given up by value while one runs, and a shared Rc(RefCell(T)) reaches this only through Rc.try_unwrap, which needs the only handle.

The value is bound out of the cell's Box: a copy, which the box's drop then releases. A move-only T is therefore rejected (E0901), since nothing moves a payload out of a Box bitwise yet (issues/2026-10-23-a-move-only-payload-cannot-move-out-of-a-box.md).

Returns: T

_swap : (fn(slot : *_RefCellSlot(T), value : T, mark : isize) -> T)

Move the slot's value out and value in, leaving mark as the slot's mark; the caller has checked the old mark.

Returns: T

impl(generic(T : Type), where(T <: Default), RefCell(T), ...)
take : (fn(self : &Self) -> T)

Store T's default and return the value it replaces — Rust's take. Panics as replace does.

Returns: T

impl(generic(T : Type), where(T <: Clone), RefCell(T), Clone(...))
clone : (fn(self : &Self) -> Self)

Create an independent clone of self.

Returns: Self