Immutable nested-state updates through a command-spec DSL.
Apply a declarative spec to a value and get back a new value. Only the touched parts are copied. Everything else is shared with the input by reference, so an unchanged branch stays pointer-identical. That sharing makes cheap identity checks possible after an update.
The spec is a tree of $-prefixed commands and child key paths, inspired by
MongoDB update operators.
[dependencies]
immutability-helper = "0.1"| Command | Target | Effect |
|---|---|---|
$push |
array | append items |
$unshift |
array | prepend items |
$splice |
array | run each splice in order |
$set |
any | replace the value |
$toggle |
object | flip boolean fields |
$unset |
object | remove keys |
$merge |
object | shallow-merge keys |
$add |
Map or Set | add entries or values |
$remove |
Map or Set | remove entries or values |
$apply |
any | replace with f(current) |
A bare function is shorthand for $apply.
Push onto an array:
use immutability_helper::{update, Spec, Value};
let original = Value::array(vec![Value::from("x")]);
let result = update(&original, &Spec::push(vec![Value::from("y")])).unwrap();
assert_eq!(result, Value::array(vec![Value::from("x"), Value::from("y")]));Set a nested value while keeping untouched branches shared:
use immutability_helper::{update, Spec, Value};
let original = Value::object(vec![
("a", Value::object(vec![("b", Value::from(1))])),
("c", Value::object(vec![("d", Value::from(2))])),
]);
let spec = Spec::at("a", Spec::at("b", Spec::set(Value::from(2))));
let result = update(&original, &spec).unwrap();
let (Value::Object(orig), Value::Object(next)) = (&original, &result) else {
unreachable!()
};
// The c branch was not touched, so it is the same allocation.
assert!(orig["c"].same_value(&next["c"]));Container values hold their contents behind Rc. A no-op update returns the
original reference. Value::same_value reports whether two values share an
allocation. Use it to tell whether an update changed a node.
The comparison function is pluggable. The default uses === semantics:
references for containers, values for primitives. A Context can install a
deeper comparison so value-preserving updates collapse back to the original.
A Context carries its own command table. extend registers a directive. The
callback receives the spec value, the current value, and the original target.
use immutability_helper::{Context, Spec, Value};
let mut ctx = Context::new();
ctx.extend("$addtax", |tax, original, _orig| {
let tax = if let Value::Number(n) = tax { *n } else { 0.0 };
let base = if let Value::Number(n) = original { *n } else { 0.0 };
Ok(Value::Number(base + tax * base))
});
let result = ctx
.update(&Value::from(5), &Spec::custom("$addtax", Value::from(0.10)))
.unwrap();
assert_eq!(result, Value::Number(5.5));This crate models plain data only. It has no prototype chains and no symbol keys. Object keys are strings in insertion order. Arrays do not carry extra non-index properties.
Map and Set members are compared by value, not by reference. $add and
$remove treat a structurally equal value as already present. Adding a value
that equals an existing member is a no-op, and removing a value that equals a
member drops it even when it is a different allocation. The data model keys Map
and Set contents by structure, so a Map or Set holds at most one entry for any
given value.
Licensed under the MIT license.