radix_immutable 0.1.0

Generic immutable radix trie data-structure.
Documentation

Radix Immutable

An immutable radix trie in Rust with structural sharing via Arc. Each mutation returns a new trie, sharing unchanged subtrees with the original.

Originally forked from radix_trie and substantially rewritten.

Features

  • Immutable API — insert/remove return new tries; originals are unchanged
  • Structural sharing — unchanged subtrees are shared via Arc, making clones cheap
  • Longest prefix match — find the most specific stored prefix for a key (routing, path resolution)
  • Prefix iteration — iterate all entries under a prefix without scanning the full trie
  • Prefix views — lightweight subtrie handles for comparison and lookup
  • Structural hashing — lazily cached hashes for fast subtree equality checks
  • BTreeMap children — deterministic iteration order, no sort overhead
  • Serde support — optional serde feature flag for serialization
  • Key generic — supports any type convertible to bytes (String, Vec<u8>, custom types)

Usage

[dependencies]
radix_immutable = "0.1"

# With serde support:
radix_immutable = { version = "0.1", features = ["serde"] }

Quick start

use radix_immutable::StringTrie;

let trie = StringTrie::<String, i32>::new()
    .insert("hello".to_string(), 1)
    .insert("help".to_string(), 2)
    .insert("world".to_string(), 3);

// Lookup
assert_eq!(trie.get(&"hello".to_string()), Some(&1));

// Longest prefix match
let trie = trie.insert("/api".to_string(), 10)
    .insert("/api/users".to_string(), 20);
assert_eq!(
    trie.longest_prefix_match(&"/api/users/123".to_string()),
    Some((&"/api/users".to_string(), &20))
);

// Prefix iteration
let hel_entries: Vec<_> = trie.iter_prefix("hel".to_string()).collect();
assert_eq!(hel_entries.len(), 2); // "hello" and "help"

// Collect from iterator
let trie: StringTrie<String, i32> = vec![
    ("a".to_string(), 1),
    ("b".to_string(), 2),
].into_iter().collect();

Performance

Why not just use a HashMap?

Prefix operations on a trie are O(key length), independent of collection size. A HashMap must scan every entry.

With 1,000 path-like keys:

Operation Trie HashMap Speedup
Longest prefix match 21 ns 3.1 µs ~150x
Iterate entries with prefix 59 ns 2.7 µs ~46x

Structural sharing vs. cloning

Inserting into an immutable trie shares unchanged subtrees via Arc. A HashMap must clone the entire map to get a new version.

With 1,000 entries, inserting one new key:

Time
Trie (structural sharing) 218 ns
HashMap (clone + insert) 27 µs

The trie is ~124x faster because it only allocates nodes along the new key's path.

Memory: snapshots via structural sharing

Keeping 10 snapshots of a 500-entry collection (each with one additional key):

Memory
Trie (10 versions, structural sharing) 12 KB
HashMap (10 full clones) 424 KB

The trie uses ~36x less memory because each version shares unchanged subtrees with the original.

Functional patterns

Immutable data structures enable patterns that are expensive or awkward with mutable collections. With structural sharing, the trie handles these naturally:

Pattern Trie HashMap Speedup
Undo stack (100 edits, keep all versions) 40 µs 228 µs ~6x
Branching (20 forks from 500-entry base) 11 µs 283 µs ~26x
Transaction rollback (10 batches, rollback half) 14 µs 164 µs ~12x

The trie's advantage grows with collection size — branching from a 500-entry base is ~26x faster because the trie forks via Arc, while the HashMap must clone all 500 entries for each branch.

Lookup scaling

Getting 100 keys from tries of increasing size (lookup time depends on key length, not collection size):

Trie size Time (100 lookups)
100 2.0 µs
1,000 2.2 µs
10,000 3.0 µs

Run benchmarks yourself with cargo bench.

Documentation

https://docs.rs/radix_immutable/

Original radix_trie Contributors

License

MIT License. Copyright Micah Fitch, Michael Sproul and contributors 2015-present.