Skip to main content

entropy_map/
map_with_dict.rs

1//! A module providing `MapWithDict`, an immutable hash map implementation.
2//!
3//! `MapWithDict` is a hash map structure that optimizes for space by utilizing a minimal perfect
4//! hash function (MPHF) for indexing the map's keys. This enables efficient storage and retrieval,
5//! as it reduces the overall memory footprint by packing unique values into a dictionary. The MPHF
6//! provides direct access to the indices of keys, which correspond to their respective values in
7//! the values dictionary. Keys are stored to ensure that `get` operation will return `None` if key
8//! wasn't present in original set.
9
10use std::borrow::Borrow;
11use std::collections::HashMap;
12use std::hash::{Hash, Hasher};
13use std::mem::size_of_val;
14
15use num::{PrimInt, Unsigned};
16use wyhash::WyHash;
17
18use crate::mphf::{Mphf, MphfError, DEFAULT_GAMMA};
19
20/// An efficient, immutable hash map with values dictionary-packed for optimized space usage.
21#[derive(Default)]
22#[cfg_attr(feature = "rkyv_derive", derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize))]
23#[cfg_attr(feature = "rkyv_derive", archive_attr(derive(rkyv::CheckBytes)))]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25#[cfg_attr(
26    feature = "serde",
27    serde(bound(serialize = "K: serde::Serialize, V: serde::Serialize, ST: serde::Serialize"))
28)]
29pub struct MapWithDict<K, V, const B: usize = 32, const S: usize = 8, ST = u8, H = WyHash>
30where
31    ST: PrimInt + Unsigned,
32    H: Hasher + Default,
33{
34    /// Minimally Perfect Hash Function for keys indices retrieval
35    mphf: Mphf<B, S, ST, H>,
36    /// Map keys
37    keys: Box<[K]>,
38    /// Points to the value index in the dictionary
39    values_index: Box<[usize]>,
40    /// Map unique values
41    values_dict: Box<[V]>,
42}
43
44#[cfg(feature = "serde")]
45#[derive(serde::Deserialize)]
46#[serde(bound(deserialize = "K: serde::Deserialize<'de>, V: serde::Deserialize<'de>, ST: serde::Deserialize<'de>"))]
47struct MapWithDictUnchecked<K, V, const B: usize = 32, const S: usize = 8, ST = u8, H = WyHash>
48where
49    ST: PrimInt + Unsigned,
50    H: Hasher + Default,
51{
52    mphf: Mphf<B, S, ST, H>,
53    keys: Box<[K]>,
54    values_index: Box<[usize]>,
55    values_dict: Box<[V]>,
56}
57
58#[cfg(feature = "serde")]
59impl<'de, K, V, const B: usize, const S: usize, ST, H> serde::Deserialize<'de> for MapWithDict<K, V, B, S, ST, H>
60where
61    K: serde::Deserialize<'de> + Hash,
62    V: serde::Deserialize<'de>,
63    ST: serde::Deserialize<'de> + PrimInt + Unsigned,
64    H: Hasher + Default,
65{
66    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67    where
68        D: serde::Deserializer<'de>,
69    {
70        use crate::{ValidateKeyResult, ValidateValueResult};
71        use serde::de::Error;
72
73        let this = MapWithDictUnchecked::<K, V, B, S, ST, H>::deserialize(deserializer)?;
74
75        this.mphf.validate_keys(&this.keys).map_err(|e| match e {
76            ValidateKeyResult::InvalidKeyCount => {
77                Error::custom("key count should equal the number of set bits in the MPHF")
78            }
79            ValidateKeyResult::IncorrectKeyOrder => Error::custom("keys should correspond to MPHF index"),
80        })?;
81
82        this.mphf
83            .validate_values(&this.keys, &this.values_index, &this.values_dict)
84            .map_err(|e| match e {
85                ValidateValueResult::KeyValueLenMismatch => Error::custom("key count should equal value count"),
86                ValidateValueResult::InvalidValueIndex => Error::custom("value index is out of bounds of value dict"),
87            })?;
88
89        Ok(Self {
90            mphf: this.mphf,
91            keys: this.keys,
92            values_index: this.values_index,
93            values_dict: this.values_dict,
94        })
95    }
96}
97
98impl<K, V, const B: usize, const S: usize, ST, H> MapWithDict<K, V, B, S, ST, H>
99where
100    K: Eq + Hash + Clone,
101    V: Eq + Clone + Hash,
102    ST: PrimInt + Unsigned,
103    H: Hasher + Default,
104{
105    /// Constructs a `MapWithDict` from an iterator of key-value pairs and MPHF function params.
106    pub fn from_iter_with_params<I>(iter: I, gamma: f32) -> Result<Self, MphfError>
107    where
108        I: IntoIterator<Item = (K, V)>,
109    {
110        let mut keys = vec![];
111        let mut values_index = vec![];
112        let mut values_dict = vec![];
113        let mut offsets_cache = HashMap::new();
114
115        for (k, v) in iter {
116            keys.push(k.clone());
117
118            if let Some(&offset) = offsets_cache.get(&v) {
119                // re-use dictionary offset if found in cache
120                values_index.push(offset);
121            } else {
122                // store current dictionary length as an offset in both index and cache
123                let offset = values_dict.len();
124                offsets_cache.insert(v.clone(), offset);
125                values_index.push(offset);
126                values_dict.push(v.clone());
127            }
128        }
129
130        let mphf = Mphf::from_slice(&keys, gamma)?;
131
132        // Re-order `keys` and `values_index` according to `mphf`
133        for i in 0..keys.len() {
134            loop {
135                let idx = mphf.get(&keys[i]).unwrap();
136                if idx == i {
137                    break;
138                }
139                keys.swap(i, idx);
140                values_index.swap(i, idx);
141            }
142        }
143
144        Ok(MapWithDict {
145            mphf,
146            keys: keys.into_boxed_slice(),
147            values_index: values_index.into_boxed_slice(),
148            values_dict: values_dict.into_boxed_slice(),
149        })
150    }
151
152    /// Returns a reference to the value corresponding to the key. Returns `None` if the key is
153    /// not present in the map.
154    ///
155    /// # Examples
156    /// ```
157    /// # use std::collections::HashMap;
158    /// # use entropy_map::MapWithDict;
159    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
160    /// assert_eq!(map.get(&1), Some(&2));
161    /// assert_eq!(map.get(&5), None);
162    /// ```
163    #[inline]
164    pub fn get<Q>(&self, key: &Q) -> Option<&V>
165    where
166        K: Borrow<Q> + PartialEq<Q>,
167        Q: Hash + Eq + ?Sized,
168    {
169        let idx = self.mphf.get(key)?;
170
171        // SAFETY: `idx` is always within bounds (ensured during construction)
172        unsafe {
173            if self.keys.get_unchecked(idx) == key {
174                // SAFETY: `idx` and `value_idx` are always within bounds (ensure during construction)
175                let value_idx = *self.values_index.get_unchecked(idx);
176                Some(self.values_dict.get_unchecked(value_idx))
177            } else {
178                None
179            }
180        }
181    }
182
183    /// Returns the number of key-value pairs in the map.
184    ///
185    /// # Examples
186    /// ```
187    /// # use std::collections::HashMap;
188    /// # use entropy_map::MapWithDict;
189    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
190    /// assert_eq!(map.len(), 2);
191    /// ```
192    #[inline]
193    pub fn len(&self) -> usize {
194        self.keys.len()
195    }
196
197    /// Returns `true` if the map contains no elements.
198    ///
199    /// # Examples
200    /// ```
201    /// # use std::collections::HashMap;
202    /// # use entropy_map::MapWithDict;
203    /// let map = MapWithDict::try_from(HashMap::from([(0, 0); 0])).unwrap();
204    /// assert_eq!(map.is_empty(), true);
205    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
206    /// assert_eq!(map.is_empty(), false);
207    /// ```
208    #[inline]
209    pub fn is_empty(&self) -> bool {
210        self.keys.is_empty()
211    }
212
213    /// Checks if the map contains the specified key.
214    ///
215    /// # Examples
216    /// ```
217    /// # use std::collections::HashMap;
218    /// # use entropy_map::MapWithDict;
219    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
220    /// assert_eq!(map.contains_key(&1), true);
221    /// assert_eq!(map.contains_key(&2), false);
222    /// ```
223    #[inline]
224    pub fn contains_key<Q>(&self, key: &Q) -> bool
225    where
226        K: Borrow<Q> + PartialEq<Q>,
227        Q: Hash + Eq + ?Sized,
228    {
229        if let Some(idx) = self.mphf.get(key) {
230            // SAFETY: `idx` is always within bounds (ensured during construction)
231            unsafe { self.keys.get_unchecked(idx) == key }
232        } else {
233            false
234        }
235    }
236
237    /// Returns an iterator over the map, yielding key-value pairs.
238    ///
239    /// # Examples
240    /// ```
241    /// # use std::collections::HashMap;
242    /// # use entropy_map::MapWithDict;
243    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
244    /// for (key, val) in map.iter() {
245    ///     println!("key: {key} val: {val}");
246    /// }
247    /// ```
248    #[inline]
249    pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
250        self.keys
251            .iter()
252            .zip(self.values_index.iter())
253            .map(move |(key, &value_idx)| {
254                // SAFETY: `value_idx` is always within bounds (ensured during construction)
255                let value = unsafe { self.values_dict.get_unchecked(value_idx) };
256                (key, value)
257            })
258    }
259
260    /// Returns an iterator over the keys of the map.
261    ///
262    /// # Examples
263    /// ```
264    /// # use std::collections::HashMap;
265    /// # use entropy_map::MapWithDict;
266    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
267    /// for key in map.keys() {
268    ///     println!("{key}");
269    /// }
270    /// ```
271    #[inline]
272    pub fn keys(&self) -> impl Iterator<Item = &K> {
273        self.keys.iter()
274    }
275
276    /// Returns an iterator over the values of the map.
277    ///
278    /// # Examples
279    /// ```
280    /// # use std::collections::HashMap;
281    /// # use entropy_map::MapWithDict;
282    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
283    /// for val in map.values() {
284    ///     println!("{val}");
285    /// }
286    /// ```
287    #[inline]
288    pub fn values(&self) -> impl Iterator<Item = &V> {
289        self.values_index.iter().map(move |&value_idx| {
290            // SAFETY: `value_idx` is always within bounds (ensured during construction)
291            unsafe { self.values_dict.get_unchecked(value_idx) }
292        })
293    }
294
295    /// Returns the total number of bytes occupied by the structure.
296    ///
297    /// # Examples
298    /// ```
299    /// # use std::collections::HashMap;
300    /// # use entropy_map::MapWithDict;
301    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
302    /// assert_eq!(map.size(), 270);
303    /// ```
304    #[inline]
305    pub fn size(&self) -> usize {
306        size_of_val(self)
307            + self.mphf.size()
308            + size_of_val(self.keys.as_ref())
309            + size_of_val(self.values_index.as_ref())
310            + size_of_val(self.values_dict.as_ref())
311    }
312}
313
314/// Creates a `MapWithDict` from a `HashMap`.
315impl<K, V> TryFrom<HashMap<K, V>> for MapWithDict<K, V>
316where
317    K: Eq + Hash + Clone,
318    V: Eq + Clone + Hash,
319{
320    type Error = MphfError;
321
322    #[inline]
323    fn try_from(value: HashMap<K, V>) -> Result<Self, Self::Error> {
324        MapWithDict::<K, V>::from_iter_with_params(value, DEFAULT_GAMMA)
325    }
326}
327
328/// Implement `get` for `Archived` version of `MapWithDict` if feature is enabled
329#[cfg(feature = "rkyv_derive")]
330impl<K, V, const B: usize, const S: usize, ST, H> ArchivedMapWithDict<K, V, B, S, ST, H>
331where
332    K: PartialEq + Hash + rkyv::Archive,
333    K::Archived: PartialEq<K>,
334    V: rkyv::Archive,
335    ST: PrimInt + Unsigned + rkyv::Archive<Archived = ST>,
336    H: Hasher + Default,
337{
338    /// Checks if the map contains the specified key.
339    ///
340    /// # Examples
341    /// ```
342    /// # use std::collections::HashMap;
343    /// # use entropy_map::MapWithDict;
344    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
345    /// let archived_map = rkyv::from_bytes::<MapWithDict<u32, u32>>(
346    ///     &rkyv::to_bytes::<_, 1024>(&map).unwrap()
347    /// ).unwrap();
348    /// assert_eq!(archived_map.contains_key(&1), true);
349    /// assert_eq!(archived_map.contains_key(&2), false);
350    /// ```
351    #[inline]
352    pub fn contains_key<Q>(&self, key: &Q) -> bool
353    where
354        K: Borrow<Q>,
355        <K as rkyv::Archive>::Archived: PartialEq<Q>,
356        Q: ?Sized + Hash + Eq,
357    {
358        if let Some(idx) = self.mphf.get(key) {
359            // SAFETY: `idx` is always within bounds (ensured during construction)
360            unsafe { self.keys.get_unchecked(idx) == key }
361        } else {
362            false
363        }
364    }
365
366    /// Returns a reference to the value corresponding to the key. Returns `None` if the key is
367    /// not present in the map.
368    ///
369    /// # Examples
370    /// ```
371    /// # use std::collections::HashMap;
372    /// # use entropy_map::MapWithDict;
373    /// let map = MapWithDict::try_from(HashMap::from([(1, 2), (3, 4)])).unwrap();
374    /// let archived_map = rkyv::from_bytes::<MapWithDict<u32, u32>>(
375    ///     &rkyv::to_bytes::<_, 1024>(&map).unwrap()
376    /// ).unwrap();
377    /// assert_eq!(archived_map.get(&1), Some(&2));
378    /// assert_eq!(archived_map.get(&5), None);
379    /// ```
380    #[inline]
381    pub fn get<Q>(&self, key: &Q) -> Option<&V::Archived>
382    where
383        K: Borrow<Q>,
384        <K as rkyv::Archive>::Archived: PartialEq<Q>,
385        Q: ?Sized + Hash + Eq,
386    {
387        let idx = self.mphf.get(key)?;
388
389        // SAFETY: `idx` is always within bounds (ensured during construction)
390        unsafe {
391            if self.keys.get_unchecked(idx) == key {
392                // SAFETY: `idx` and `value_idx` are always within bounds (ensure during construction)
393                let value_idx = *self.values_index.get_unchecked(idx) as usize;
394                Some(self.values_dict.get_unchecked(value_idx))
395            } else {
396                None
397            }
398        }
399    }
400
401    /// Returns an iterator over the archived map, yielding archived key-value pairs.
402    #[inline]
403    pub fn iter(&self) -> impl Iterator<Item = (&K::Archived, &V::Archived)> {
404        self.keys
405            .iter()
406            .zip(self.values_index.iter())
407            .map(move |(key, &value_idx)| {
408                // SAFETY: `value_idx` is always within bounds (ensured during construction)
409                let value = unsafe { self.values_dict.get_unchecked(value_idx as usize) };
410                (key, value)
411            })
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    use paste::paste;
419    use proptest::prelude::*;
420    use rand::{Rng, SeedableRng};
421    use rand_chacha::ChaCha8Rng;
422    use std::collections::{hash_map::RandomState, HashSet};
423
424    fn gen_map(items_num: usize) -> HashMap<u64, u32> {
425        let mut rng = ChaCha8Rng::seed_from_u64(123);
426
427        (0..items_num)
428            .map(|_| {
429                let key = rng.gen::<u64>();
430                let value = rng.gen_range(1..=10);
431                (key, value)
432            })
433            .collect()
434    }
435
436    #[test]
437    fn test_map_with_dict() {
438        // Collect original key-value pairs directly into a HashMap
439        let original_map = gen_map(1000);
440
441        // Create the map from the iterator
442        let map = MapWithDict::try_from(original_map.clone()).unwrap();
443
444        // Test len
445        assert_eq!(map.len(), original_map.len());
446
447        // Test is_empty
448        assert_eq!(map.is_empty(), original_map.is_empty());
449
450        // Test get, contains_key
451        for (key, value) in &original_map {
452            assert_eq!(map.get(key), Some(value));
453            assert!(map.contains_key(key));
454        }
455
456        // Test iter
457        for (&k, &v) in map.iter() {
458            assert_eq!(original_map.get(&k), Some(&v));
459        }
460
461        // Test keys
462        for k in map.keys() {
463            assert!(original_map.contains_key(k));
464        }
465
466        // Test values
467        for &v in map.values() {
468            assert!(original_map.values().any(|&val| val == v));
469        }
470
471        // Test size
472        assert_eq!(map.size(), 16626);
473    }
474
475    /// Assert that we can call `.get()` with `K::borrow()`.
476    #[test]
477    fn test_get_borrow() {
478        let original_map = HashMap::from_iter([("a".to_string(), ()), ("b".to_string(), ())]);
479        let map = MapWithDict::try_from(original_map).unwrap();
480
481        assert_eq!(map.get("a"), Some(&()));
482        assert!(map.contains_key("a"));
483        assert_eq!(map.get("b"), Some(&()));
484        assert!(map.contains_key("b"));
485        assert_eq!(map.get("c"), None);
486        assert!(!map.contains_key("c"));
487    }
488
489    #[cfg(feature = "rkyv_derive")]
490    #[test]
491    fn test_rkyv() {
492        // create regular `HashMap`, then `MapWithDict`, then serialize to `rkyv` bytes.
493        let original_map = gen_map(1000);
494        let map = MapWithDict::try_from(original_map.clone()).unwrap();
495        let rkyv_bytes = rkyv::to_bytes::<_, 1024>(&map).unwrap();
496
497        let rkyv_map = rkyv::check_archived_root::<MapWithDict<u64, u32>>(&rkyv_bytes).unwrap();
498
499        // Test get on `Archived` version
500        for (k, v) in original_map.iter() {
501            assert_eq!(v, rkyv_map.get(k).unwrap());
502        }
503
504        // Test iter on `Archived` version
505        for (&k, &v) in rkyv_map.iter() {
506            assert_eq!(original_map.get(&k), Some(&v));
507        }
508    }
509
510    #[cfg(feature = "rkyv_derive")]
511    #[test]
512    fn test_rkyv_get_borrow() {
513        let original_map = HashMap::from_iter([("a".to_string(), ()), ("b".to_string(), ())]);
514        let map = MapWithDict::try_from(original_map).unwrap();
515        let rkyv_bytes = rkyv::to_bytes::<_, 1024>(&map).unwrap();
516        let rkyv_map = rkyv::check_archived_root::<MapWithDict<String, ()>>(&rkyv_bytes).unwrap();
517
518        assert_eq!(map.get("a"), Some(&()));
519        assert!(rkyv_map.contains_key("a"));
520        assert_eq!(map.get("b"), Some(&()));
521        assert!(rkyv_map.contains_key("b"));
522        assert_eq!(map.get("c"), None);
523        assert!(!rkyv_map.contains_key("c"));
524    }
525
526    #[cfg(feature = "serde")]
527    #[test]
528    fn test_serde() {
529        // create regular `HashMap`, then `MapWithDict`, then serialize to msgpack bytes.
530        let original_map = gen_map(1000);
531        let map = MapWithDict::try_from(original_map.clone()).unwrap();
532
533        let bytes = rmp_serde::to_vec(&map).unwrap();
534        let de: MapWithDict<u64, u32> = rmp_serde::from_slice(&bytes).unwrap();
535
536        assert_eq!(de.len(), original_map.len());
537
538        // Test get on the deserialized `MapWithDict`
539        for (k, v) in original_map.iter() {
540            assert_eq!(de.get(k), Some(v));
541        }
542
543        // Test iter on the deserialized `MapWithDict`
544        for (&k, &v) in de.iter() {
545            assert_eq!(original_map.get(&k), Some(&v));
546        }
547    }
548
549    #[cfg(feature = "serde")]
550    #[test]
551    fn test_serde_get_borrow() {
552        let original_map = HashMap::from_iter([("a".to_string(), ()), ("b".to_string(), ())]);
553        let map = MapWithDict::try_from(original_map).unwrap();
554        let bytes = rmp_serde::to_vec(&map).unwrap();
555        let de: MapWithDict<String, ()> = rmp_serde::from_slice(&bytes).unwrap();
556
557        assert_eq!(de.get("a"), Some(&()));
558        assert!(de.contains_key("a"));
559        assert_eq!(de.get("b"), Some(&()));
560        assert!(de.contains_key("b"));
561        assert_eq!(de.get("c"), None);
562        assert!(!de.contains_key("c"));
563    }
564
565    macro_rules! proptest_map_with_dict_model {
566        ($(($b:expr, $s:expr, $gamma:expr)),* $(,)?) => {
567            $(
568                paste! {
569                    proptest! {
570                        #[test]
571                        fn [<proptest_map_with_dict_model_ $b _ $s _ $gamma>](model: HashMap<u64, u64>, arbitrary: HashSet<u64>) {
572                            let entropy_map: MapWithDict<u64, u64, $b, $s> = MapWithDict::from_iter_with_params(
573                                model.clone(),
574                                $gamma as f32 / 100.0
575                            ).unwrap();
576
577                            // Assert that length matches model.
578                            assert_eq!(entropy_map.len(), model.len());
579                            assert_eq!(entropy_map.is_empty(), model.is_empty());
580
581                            // Assert that keys and values match model.
582                            assert_eq!(
583                                HashSet::<_, RandomState>::from_iter(entropy_map.keys()),
584                                HashSet::from_iter(model.keys())
585                            );
586                            assert_eq!(
587                                HashSet::<_, RandomState>::from_iter(entropy_map.values()),
588                                HashSet::from_iter(model.values())
589                            );
590
591                            // Assert that contains and get operations match model for contained elements.
592                            for (k, v) in &model {
593                                assert!(entropy_map.contains_key(&k));
594                                assert_eq!(entropy_map.get(&k), Some(v));
595                            }
596
597                            // Assert that contains and get operations match model for random elements.
598                            for k in arbitrary {
599                                assert_eq!(
600                                    model.contains_key(&k),
601                                    entropy_map.contains_key(&k),
602                                );
603                                assert_eq!(entropy_map.get(&k), model.get(&k));
604                            }
605                        }
606                    }
607                }
608            )*
609        };
610    }
611
612    proptest_map_with_dict_model!(
613        // (1, 8, 100),
614        (2, 8, 100),
615        (4, 8, 100),
616        (7, 8, 100),
617        (8, 8, 100),
618        (15, 8, 100),
619        (16, 8, 100),
620        (23, 8, 100),
621        (24, 8, 100),
622        (31, 8, 100),
623        (32, 8, 100),
624        (33, 8, 100),
625        (48, 8, 100),
626        (53, 8, 100),
627        (61, 8, 100),
628        (63, 8, 100),
629        (64, 8, 100),
630        (32, 7, 100),
631        (32, 5, 100),
632        (32, 4, 100),
633        (32, 3, 100),
634        (32, 1, 100),
635        (32, 0, 100),
636        (32, 8, 200),
637        (32, 6, 200),
638    );
639}