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
serdefeature flag for serialization - Key generic — supports any type convertible to bytes (
String,Vec<u8>, custom types)
Usage
[]
= "0.1"
# With serde support:
= { = "0.1", = ["serde"] }
Quick start
use StringTrie;
let trie = new
.insert
.insert
.insert;
// Lookup
assert_eq!;
// Longest prefix match
let trie = trie.insert
.insert;
assert_eq!;
// Prefix iteration
let hel_entries: = trie.iter_prefix.collect;
assert_eq!; // "hello" and "help"
// Collect from iterator
let trie: = vec!.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
- Allan Simon (@allan-simon)
- Andrew Smith (@andrewcsmith)
- Arthur Carcano (@NougatRillettes)
- Devin Ragotzy (@DevinR528)
- @hanabi1224
- Jakob Dalsgaard (@jakobdalsgaard)
- Michael Sproul (@michaelsproul)
- Robin Lambertz (@roblabla)
- Sergey (@Albibek)
- Stuart Hinson (@stuarth)
- Vinzent Steinberg (@vks)
License
MIT License. Copyright Micah Fitch, Michael Sproul and contributors 2015-present.