Skip to content

Latest commit

 

History

20 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

immutability-helper

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.

Installation

[dependencies]
immutability-helper = "0.1"

Commands

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.

Examples

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"]));

Reference equality

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.

Custom commands

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));

Scope

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.

License

Licensed under the MIT license.

About

immutable nested-state updates via a command-spec DSL

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages