Rust Persistent Data Structures provides fully persistent data structures with structural sharing.
To use rpds add the following to your Cargo.toml
:
toml
[dependencies]
rpds = "<version>"
This crate offers the following data structures:
List
Your classic functional list.
```rust use rpds::List;
let list = List::new().push_front("list");
assert_eq!(list.first(), Some(&"list"));
let alist = list.pushfront("a");
asserteq!(alist.first(), Some(&"a"));
let listdropped = alist.drop_first().unwrap();
asserteq!(listdropped, list); ```
Vector
A sequence that can be indexed. The implementation is described in Understanding Persistent Vector Part 1 and Understanding Persistent Vector Part 2.
```rust use rpds::Vector;
let vector = Vector::new() .pushback("I’m") .pushback("a") .push_back("vector");
assert_eq!(vector[1], "a");
let screamingvector = vector .droplast().unwrap() .push_back("VECTOR!!!");
asserteq!(screamingvector[2], "VECTOR!!!"); ```
Stack
A LIFO (last in, first out) data structure. This is just a List
in disguise.
```rust use rpds::Stack;
let stack = Stack::new().push("stack");
assert_eq!(stack.peek(), Some(&"stack"));
let a_stack = stack.push("a");
asserteq!(astack.peek(), Some(&"a"));
let stackpopped = astack.pop().unwrap();
asserteq!(stackpopped, stack); ```
Queue
A FIFO (first in, first out) data structure.
```rust use rpds::Queue;
let queue = Queue::new() .enqueue("um") .enqueue("dois") .enqueue("tres");
assert_eq!(queue.peek(), Some(&"um"));
let queue_dequeued = queue.dequeue().unwrap();
asserteq!(queuedequeued.peek(), Some(&"dois")); ```
HashTrieMap
A map implemented with a hash array mapped trie. See Ideal Hash Trees for details.
```rust use rpds::HashTrieMap;
let map_en = HashTrieMap::new() .insert(0, "zero") .insert(1, "one");
asserteq!(mapen.get(&1), Some(&"one"));
let mappt = mapen .insert(1, "um") .insert(2, "dois");
asserteq!(mappt.get(&2), Some(&"dois"));
let mapptbinary = map_pt.remove(&2);
asserteq!(mappt_binary.get(&2), None); ```
HashTrieSet
A set implemented with a HashTrieMap
.
```rust use rpds::HashTrieSet;
let set = HashTrieSet::new() .insert("zero") .insert("one");
assert!(set.contains(&"one"));
let set_extended = set.insert("two");
assert!(set_extended.contains(&"two"));
let setpositive = setextended.remove(&"zero");
assert!(!set_positive.contains(&"zero")); ```
RedBlackTreeMap
A map implemented with a red-black tree.
```rust use rpds::RedBlackTreeMap;
let map_en = RedBlackTreeMap::new() .insert(0, "zero") .insert(1, "one");
asserteq!(mapen.get(&1), Some(&"one"));
let mappt = mapen .insert(1, "um") .insert(2, "dois");
asserteq!(mappt.get(&2), Some(&"dois"));
let mapptbinary = map_pt.remove(&2);
asserteq!(mappt_binary.get(&2), None);
asserteq!(mappt_binary.first(), Some((&0, &"zero"))); ```
RedBlackTreeSet
A set implemented with a RedBlackTreeMap
.
```rust use rpds::RedBlackTreeSet;
let set = RedBlackTreeSet::new() .insert("zero") .insert("one");
assert!(set.contains(&"one"));
let set_extended = set.insert("two");
assert!(set_extended.contains(&"two"));
let setpositive = setextended.remove(&"zero");
assert!(!set_positive.contains(&"zero"));
asserteq!(setpositive.first(), Some(&"one")); ```
When you change a data structure you often do not need its previous versions. For those cases rpds offers you mutable methods which are generally faster:
```rust use rpds::HashTrieSet;
let mut set = HashTrieSet::new();
set.insertmut("zero"); set.insertmut("one");
let set01 = set.clone(); let set01_2 = set.insert("two"); ```
There are convenient initialization macros for all data structures:
```rust use rpds::*;
let vector = vector![3, 1, 4, 1, 5]; let map = ht_map!["orange" => "orange", "banana" => "yellow"]; ```
Check the documentation for initialization macros of other data structures.
All data structures in this crate can be shared between threads, but that is an opt-in ability. This is because there is a performance cost to make data structures thread safe. That cost is worth avoiding when you are not actually sharing them between threads.
Of course if you try to share a rpds data structure across different threads you can count on the rust compiler to ensure that it is safe to do so. If you are using the version of the data structure that is not thread safe you will get a compile-time error.
To create a thread-safe version of any data structure use new_sync()
:
rust
let vec = Vector::new_sync()
.push_back(42);
Or use the _sync
variant of the initialization macro:
rust
let vec = vector_sync!(42);
Internally the data structures in this crate maintain a lot of reference-counting pointers. These pointers are used both for links between the internal nodes of the data structure as well as for the values it stores.
There are two implementations of reference-counting pointers in the standard library:
Rc
and
Arc
. They behave the same way, but
Arc
allows you to share the data it points to across multiple threads. The downside is that
it is significantly slower to clone and drop than Rc
, and persistent data structures do a
lot of those operations. In some microbenchmarks with rpds data structure we can see that
using Rc
instead of Arc
can make some operations twice as fast! You can see this for
yourself by running cargo bench
.
To implement this we parameterize the type of reference-counting pointer (Rc
or Arc
) as a
type argument of the data structure. We use the archery
crate to do this in a convenient way.
The pointer type can be parameterized like this:
rust
let vec: Vector<u32, archery::ArcK> = Vector::new_with_ptr_kind();
// ↖
// This will use `Arc` pointers.
// Change it to `archery::RcK` to use a `Rc` pointer.
no_std
supportThis crate supports no_std
. To enable that you need to disable the default feature std
:
toml
[dependencies]
rpds = { version = "<version>", default-features = false }
We support serialization through serde. To use it
enable the serde
feature. To do so change the rpds dependency in your Cargo.toml
to
toml
[dependencies]
rpds = { version = "<version>", features = ["serde"] }