Skip to main content

entropy_map/
set.rs

1//! A module providing `Set`, an immutable set implementation backed by a MPHF.
2//!
3//! This implementation is optimized for efficient membership checks by using a MPHF to evaluate
4//! whether an item is in the set. Keys are stored in the map to ensure that queries for an item
5//! not in the set always fail.
6//!
7//! # When to use?
8//! Use this set implementation when you have a pre-defined set of keys and you want to check for
9//! efficient membership in that set. Because this set is immutable, it is not possible to
10//! dynamically update membership. However, when the `rkyv_derive` feature is enabled, you can use
11//! [`rkyv`](https://rkyv.org/) to perform zero-copy deserialization of a new set.
12
13use std::borrow::Borrow;
14use std::collections::HashSet;
15use std::hash::{Hash, Hasher};
16use std::mem::size_of_val;
17
18use num::{PrimInt, Unsigned};
19use wyhash::WyHash;
20
21use crate::mphf::{Mphf, MphfError, DEFAULT_GAMMA};
22
23/// An efficient, immutable set.
24#[derive(Default)]
25#[cfg_attr(feature = "rkyv_derive", derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize))]
26#[cfg_attr(feature = "rkyv_derive", archive_attr(derive(rkyv::CheckBytes)))]
27#[cfg_attr(feature = "serde", derive(serde::Serialize))]
28#[cfg_attr(
29    feature = "serde",
30    serde(bound(serialize = "K: serde::Serialize, ST: serde::Serialize"))
31)]
32pub struct Set<K, const B: usize = 32, const S: usize = 8, ST = u8, H = WyHash>
33where
34    ST: PrimInt + Unsigned,
35    H: Hasher + Default,
36{
37    /// Minimally Perfect Hash Function for keys indices retrieval
38    pub(crate) mphf: Mphf<B, S, ST, H>,
39    /// Set keys
40    keys: Box<[K]>,
41}
42
43#[cfg(feature = "serde")]
44#[derive(serde::Deserialize)]
45#[serde(bound(deserialize = "K: serde::Deserialize<'de>, ST: serde::Deserialize<'de>"))]
46struct SetUnchecked<K, const B: usize = 32, const S: usize = 8, ST = u8, H = WyHash>
47where
48    ST: PrimInt + Unsigned,
49    H: Hasher + Default,
50{
51    pub(crate) mphf: Mphf<B, S, ST, H>,
52    keys: Box<[K]>,
53}
54
55#[cfg(feature = "serde")]
56impl<'de, K, const B: usize, const S: usize, ST, H> serde::Deserialize<'de> for Set<K, B, S, ST, H>
57where
58    K: serde::Deserialize<'de> + Hash,
59    ST: serde::Deserialize<'de> + PrimInt + Unsigned,
60    H: Hasher + Default,
61{
62    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63    where
64        D: serde::Deserializer<'de>,
65    {
66        use crate::ValidateKeyResult;
67        use serde::de::Error;
68
69        let this = SetUnchecked::deserialize(deserializer)?;
70
71        this.mphf.validate_keys(&this.keys).map_err(|e| match e {
72            ValidateKeyResult::InvalidKeyCount => {
73                Error::custom("key count should equal the number of set bits in the MPHF")
74            }
75            ValidateKeyResult::IncorrectKeyOrder => Error::custom("keys should correspond to MPHF index"),
76        })?;
77
78        Ok(Self { mphf: this.mphf, keys: this.keys })
79    }
80}
81
82impl<K, const B: usize, const S: usize, ST, H> Set<K, B, S, ST, H>
83where
84    K: Eq + Hash,
85    ST: PrimInt + Unsigned,
86    H: Hasher + Default,
87{
88    /// Constructs a `Set` from an iterator of keys and MPHF function parameters.
89    ///
90    /// # Examples
91    /// ```
92    /// use entropy_map::{Set, DEFAULT_GAMMA};
93    ///
94    /// let set: Set<u32> = Set::from_iter_with_params([1, 2, 3], DEFAULT_GAMMA).unwrap();
95    /// assert!(set.contains(&1));
96    /// ```
97    pub fn from_iter_with_params<I>(iter: I, gamma: f32) -> Result<Self, MphfError>
98    where
99        I: IntoIterator<Item = K>,
100    {
101        let mut keys: Vec<K> = iter.into_iter().collect();
102
103        let mphf = Mphf::from_slice(&keys, gamma)?;
104
105        // Re-order `keys` and according to `mphf`
106        for i in 0..keys.len() {
107            loop {
108                let idx: usize = mphf.get(&keys[i]).unwrap();
109                if idx == i {
110                    break;
111                }
112                keys.swap(i, idx);
113            }
114        }
115
116        Ok(Set { mphf, keys: keys.into_boxed_slice() })
117    }
118
119    /// Returns `true` if the set contains the value.
120    ///
121    /// # Examples
122    /// ```
123    /// # use std::collections::HashSet;
124    /// # use entropy_map::Set;
125    /// let set = Set::try_from(HashSet::from([1, 2, 3])).unwrap();
126    /// assert_eq!(set.contains(&1), true);
127    /// assert_eq!(set.contains(&4), false);
128    /// ```
129    #[inline]
130    pub fn contains<Q>(&self, key: &Q) -> bool
131    where
132        K: Borrow<Q> + PartialEq<Q>,
133        Q: Hash + Eq + ?Sized,
134    {
135        // SAFETY: `idx` is always within array bounds (ensured during construction)
136        self.mphf
137            .get(key)
138            .map(|idx| unsafe { self.keys.get_unchecked(idx) == key })
139            .unwrap_or_default()
140    }
141
142    /// Returns the number of elements in the set.
143    ///
144    /// # Examples
145    /// ```
146    /// # use std::collections::HashSet;
147    /// # use entropy_map::Set;
148    /// let set = Set::try_from(HashSet::from([1, 2, 3])).unwrap();
149    /// assert_eq!(set.len(), 3);
150    /// ```
151    #[inline]
152    pub fn len(&self) -> usize {
153        self.keys.len()
154    }
155
156    /// Returns `true` if the set contains no elements.
157    ///
158    /// # Examples
159    /// ```
160    /// # use std::collections::HashSet;
161    /// # use entropy_map::Set;
162    /// let set = Set::try_from(HashSet::from([0u32; 0])).unwrap();
163    /// assert_eq!(set.is_empty(), true);
164    /// let set = Set::try_from(HashSet::from([1, 2, 3])).unwrap();
165    /// assert_eq!(set.is_empty(), false);
166    /// ```
167    #[inline]
168    pub fn is_empty(&self) -> bool {
169        self.keys.is_empty()
170    }
171
172    /// Returns an iterator visiting set elements in arbitrary order.
173    ///
174    /// # Examples
175    /// ```
176    /// # use std::collections::HashSet;
177    /// # use entropy_map::Set;
178    /// let set = Set::try_from(HashSet::from([1, 2, 3])).unwrap();
179    /// for x in set.iter() {
180    ///     println!("{x}");
181    /// }
182    /// ```
183    #[inline]
184    pub fn iter(&self) -> impl Iterator<Item = &K> {
185        self.keys.iter()
186    }
187
188    /// Returns the total number of bytes occupied by `Set`.
189    ///
190    /// # Examples
191    /// ```
192    /// # use std::collections::HashSet;
193    /// # use entropy_map::Set;
194    /// let set = Set::try_from(HashSet::from([1, 2, 3])).unwrap();
195    /// assert_eq!(set.size(), 218);
196    /// ```
197    #[inline]
198    pub fn size(&self) -> usize {
199        size_of_val(self) + self.mphf.size() + size_of_val(self.keys.as_ref())
200    }
201}
202
203/// Creates a `Set` from a `HashSet`.
204impl<K> TryFrom<HashSet<K>> for Set<K>
205where
206    K: Eq + Hash,
207{
208    type Error = MphfError;
209
210    #[inline]
211    fn try_from(value: HashSet<K>) -> Result<Self, Self::Error> {
212        Set::from_iter_with_params(value, DEFAULT_GAMMA)
213    }
214}
215
216/// Implement `contains` for `Archived` version of `Set` if feature is enabled
217#[cfg(feature = "rkyv_derive")]
218impl<K, const B: usize, const S: usize, ST, H> ArchivedSet<K, B, S, ST, H>
219where
220    K: Eq + Hash + rkyv::Archive,
221    K::Archived: PartialEq<K>,
222    ST: PrimInt + Unsigned + rkyv::Archive<Archived = ST>,
223    H: Hasher + Default,
224{
225    /// Returns `true` if the set contains the value.
226    ///
227    /// # Examples
228    /// ```
229    /// # use std::collections::HashSet;
230    /// # use entropy_map::{ArchivedSet, Set};
231    /// let set: Set<u32> = Set::try_from(HashSet::from([1, 2, 3])).unwrap();
232    /// let archived_set = rkyv::from_bytes::<Set<u32>>(
233    ///     &rkyv::to_bytes::<_, 1024>(&set).unwrap()
234    /// ).unwrap();
235    /// assert_eq!(archived_set.contains(&1), true);
236    /// assert_eq!(archived_set.contains(&4), false);
237    /// ```
238    #[inline]
239    pub fn contains<Q>(&self, key: &Q) -> bool
240    where
241        K: Borrow<Q>,
242        <K as rkyv::Archive>::Archived: PartialEq<Q>,
243        Q: ?Sized + Hash + Eq,
244    {
245        // SAFETY: `idx` is always within bounds (ensured during construction)
246        self.mphf
247            .get(key)
248            .map(|idx| unsafe { self.keys.get_unchecked(idx) == key })
249            .unwrap_or_default()
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use paste::paste;
257    use proptest::prelude::*;
258    use rand::{Rng, SeedableRng};
259    use rand_chacha::ChaCha8Rng;
260
261    fn gen_set(items_num: usize) -> HashSet<u64> {
262        let mut rng = ChaCha8Rng::seed_from_u64(123);
263
264        (0..items_num).map(|_| rng.gen::<u64>()).collect()
265    }
266
267    #[test]
268    fn test_set_with_hashset() {
269        // Collect original key-value pairs directly into a HashSet
270        let original_set = gen_set(1000);
271
272        // Create the set from the iterator
273        let set = Set::try_from(original_set.clone()).unwrap();
274
275        // Test len
276        assert_eq!(set.len(), original_set.len());
277
278        // Test is_empty
279        assert_eq!(set.is_empty(), original_set.is_empty());
280
281        // Test get, contains_key
282        for key in &original_set {
283            assert!(set.contains(key));
284        }
285
286        // Test iter
287        for &k in set.iter() {
288            assert!(original_set.contains(&k));
289        }
290
291        // Test size
292        assert_eq!(set.size(), 8540);
293    }
294
295    /// Assert that we can call `.contains()` with `K::borrow()`.
296    #[test]
297    fn test_contains_borrow() {
298        let set = Set::try_from(HashSet::from(["a".to_string(), "b".to_string()])).unwrap();
299
300        assert!(set.contains("a"));
301        assert!(set.contains("b"));
302        assert!(!set.contains("c"));
303    }
304
305    #[cfg(feature = "rkyv_derive")]
306    #[test]
307    fn test_rkyv() {
308        // create regular `HashSet`, then `Set`, then serialize to `rkyv` bytes.
309        let original_set = gen_set(1000);
310        let set = Set::try_from(original_set.clone()).unwrap();
311        let rkyv_bytes = rkyv::to_bytes::<_, 1024>(&set).unwrap();
312
313        let rkyv_set = rkyv::check_archived_root::<Set<u64>>(&rkyv_bytes).unwrap();
314
315        // Test get on `Archived` version
316        for k in original_set.iter() {
317            assert!(rkyv_set.contains(k));
318        }
319    }
320
321    #[cfg(feature = "rkyv_derive")]
322    #[test]
323    fn test_rkyv_contains_borrow() {
324        let set = Set::try_from(HashSet::from(["a".to_string(), "b".to_string()])).unwrap();
325        let rkyv_bytes = rkyv::to_bytes::<_, 1024>(&set).unwrap();
326        let rkyv_set = rkyv::check_archived_root::<Set<String>>(&rkyv_bytes).unwrap();
327
328        assert!(rkyv_set.contains("a"));
329        assert!(rkyv_set.contains("b"));
330        assert!(!rkyv_set.contains("c"));
331    }
332
333    #[cfg(feature = "serde")]
334    #[test]
335    fn test_serde() {
336        // create regular `HashSet`, then `Set`, then serialize to msgpack bytes.
337        let original_set = gen_set(1000);
338        let set = Set::try_from(original_set.clone()).unwrap();
339
340        let bytes = rmp_serde::to_vec(&set).unwrap();
341        let de: Set<u64> = rmp_serde::from_slice(&bytes).unwrap();
342
343        assert_eq!(de.len(), original_set.len());
344
345        // Test contains on the deserialized `Set`
346        for k in original_set.iter() {
347            assert!(de.contains(k));
348        }
349    }
350
351    macro_rules! proptest_set_model {
352        ($(($b:expr, $s:expr, $gamma:expr)),* $(,)?) => {
353            $(
354                paste! {
355                    proptest! {
356                        #[test]
357                        fn [<proptest_set_model_ $b _ $s _ $gamma>](model: HashSet<u64>, arbitrary: HashSet<u64>) {
358                            let entropy_set: Set<u64, $b, $s> = Set::from_iter_with_params(
359                                model.clone(),
360                                $gamma as f32 / 100.0
361                            ).unwrap();
362
363                            // Assert that length matches model.
364                            assert_eq!(entropy_set.len(), model.len());
365                            assert_eq!(entropy_set.is_empty(), model.is_empty());
366
367                            // Assert that contains operations match model for contained elements.
368                            for elm in &model {
369                                assert!(entropy_set.contains(&elm));
370                            }
371
372                            // Assert that contains operations match model for random elements.
373                            for elm in arbitrary {
374                                assert_eq!(
375                                    model.contains(&elm),
376                                    entropy_set.contains(&elm),
377                                );
378                            }
379                        }
380                    }
381                }
382            )*
383        };
384    }
385
386    proptest_set_model!(
387        // (1, 8, 100),
388        (2, 8, 100),
389        (4, 8, 100),
390        (7, 8, 100),
391        (8, 8, 100),
392        (15, 8, 100),
393        (16, 8, 100),
394        (23, 8, 100),
395        (24, 8, 100),
396        (31, 8, 100),
397        (32, 8, 100),
398        (33, 8, 100),
399        (48, 8, 100),
400        (53, 8, 100),
401        (61, 8, 100),
402        (63, 8, 100),
403        (64, 8, 100),
404        (32, 7, 100),
405        (32, 5, 100),
406        (32, 4, 100),
407        (32, 3, 100),
408        (32, 1, 100),
409        (32, 0, 100),
410        (32, 8, 200),
411        (32, 6, 200),
412    );
413
414    proptest! {
415        #[test]
416        fn test_set_contains(model: HashSet<u64>, arbitrary: HashSet<u64>) {
417            let entropy_set = Set::try_from(model.clone()).unwrap();
418
419            for elm in &model {
420                assert!(entropy_set.contains(elm));
421            }
422
423            for elm in arbitrary {
424                assert_eq!(
425                    model.contains(&elm),
426                    entropy_set.contains(&elm),
427                );
428            }
429        }
430    }
431}