Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Creating Multiple Zippers in the Same Map

Sometimes you need a write-enabled zipper along side one or more other zippers in the same map. This primarily happens if you want to:

  • Use an algebraic operation to write results into the same map that also holds the arguments
  • Use zippers to parallelize work over multiple threads

Creating multiple zippers in the same map is possible and very handy. That's what the [ZipperHead] object is designed to enable.

Making Multiple Zippers

The normal [PathMap] methods to create write-enabled zippers, ie. write_zipper and write_zipper_at_path require an &mut borrow of the map. This prevents a write-enabled zipper from being created while any other zipper simultaneously exists in that map (for reading or writing). This can be very obnoxious, especially when you are working within a single large map.

[ZipperHead] provides an alternative API to create zippers, that allows the exclusivity rules to be checked at runtime, rather than statically by the Rust borrow checker.

NOTE: Using [ZipperHead] methods comes with a high runtime cost relative to using the [PathMap] methods. So don't use a ZipperHead unless you need to.

WARNING: Creating a write zipper from a ZipperHead has the side effect of creating the path up to the zipper's root. Then dropping the write zipper will leave a dangling path if no further trie structure was added by the write zipper. If this is unacceptable, call [cleanup_write_zipper].

Zipper Exclusivity Rules

A zipper is a cursor to read and/or write into a location in a trie. By extension it is also a permission to perform that reading / writing. Analogous to the borrow checker's rules, zippers under a ZipperHead must obey the following:

For any reachable path, there can either be one write-enabled zipper, or many read-only zippers, but never both at the same time.

An example trie containing the paths: ["internal", "internet", "interval", "integer", "integral", "integration", "integrity", "intolerable", "intone"] looks like the diagram below.

Let's create a [ReadZipper] at the path: "integ" and a [WriteZipper] at the path "intern". Notice in the diagram that paths in the trie accessible to the ReadZipper are yellow. Additional ReadZippers may be created on any of these nodes.

Paths accessible to the WriteZipper are red, and they are reserved strictly for this zipper, while it exists.

The paths in blue are free for a zipper of any type to be created.

The paths in grey are not a accessible to any zippers. This is because any zipper created at those paths could descend to the exclusive red paths held by the WriteZipper.

Example: Creating Multiple Zippers in a PathMap

#![allow(unused)]
fn main() {
extern crate pathmap;
use pathmap::{PathMap, zipper::*};

// Create and populate a map
let mut map = PathMap::new();
map.set_val_at(b"data:0000:value", 100);
map.set_val_at(b"data:0001:value", 200);

// Create a ZipperHead at the map root
let zh = map.zipper_head();

// Now we can create multiple write zippers at different exclusive paths
let mut reader_0 = zh.read_zipper_at_path(b"data:0000:value").unwrap();
let mut reader_1 = zh.read_zipper_at_path(b"data:0001:value").unwrap();
let mut writer_0 = zh.write_zipper_at_exclusive_path(b"data:0000:result").unwrap();
let mut writer_1 = zh.write_zipper_at_exclusive_path(b"data:0001:result").unwrap();

// Each zipper can now independently modify its exclusive region
writer_0.set_val(*reader_0.val().unwrap() * 2);
writer_1.set_val(*reader_1.val().unwrap() * 2);

// Clean up - zippers must be dropped before the ZipperHead
drop(writer_0);
drop(writer_1);
drop(reader_0);
drop(reader_1);
drop(zh);

// Verify the results
assert_eq!(map.get_val_at(b"data:0000:result"), Some(&200));
assert_eq!(map.get_val_at(b"data:0001:result"), Some(&400));
}