🦀 Functional Rust

360: Bitset Pattern — Bitmask as Dense Boolean Set

Difficulty: 3 Level: Advanced Represent a set of small integers as a single integer where each bit is a membership flag — set union, intersection, and membership test in one CPU instruction.

The Problem This Solves

You're tracking which of 64 possible features are enabled for a user, which cells in a grid are alive (Game of Life), or which nodes in a small graph have been visited. A `HashSet<u8>` works, but it allocates heap memory, adds pointer chasing, and turns every membership test into a hash computation. A bitset compresses all of this. With 64 elements, the entire set fits in one `u64` — a single 8-byte integer. Membership test is a bitwise AND (`(mask >> i) & 1`). Union is bitwise OR. Intersection is bitwise AND. Complement is bitwise NOT. These are single CPU instructions, and modern CPUs can process 64-element sets in a single clock cycle. The pattern scales to larger sets by using arrays of `u64` (or the `bitvec` crate for arbitrary sizes). Even a 1024-element bitset fits in 128 bytes — entirely in L1 cache — where a `HashSet<u16>` would use kilobytes of heap memory with pointer indirection.

The Intuition

Python doesn't have built-in bitsets, but you can replicate the pattern: `mask | (1 << i)` to set bit i, `mask & (1 << i)` to test it. The pattern is identical in Rust, just with explicit types. The key insight: if your universe of elements is small (≤ 64) and represented as integers, a bitset is almost always the right choice over `HashSet`. You trade generality (arbitrary types) for speed (single-instruction operations) and density (8 bytes vs. heap allocation).

How It Works in Rust

// A bitset for elements 0..63 using a single u64
struct BitSet64(u64);

impl BitSet64 {
 fn new() -> Self { BitSet64(0) }

 fn insert(&mut self, i: u32) {
     assert!(i < 64);
     self.0 |= 1u64 << i; // set bit i
 }

 fn remove(&mut self, i: u32) {
     self.0 &= !(1u64 << i); // clear bit i
 }

 fn contains(&self, i: u32) -> bool {
     (self.0 >> i) & 1 == 1 // test bit i
 }

 // Set union — one CPU instruction
 fn union(&self, other: &Self) -> Self {
     BitSet64(self.0 | other.0)
 }

 // Set intersection — one CPU instruction
 fn intersection(&self, other: &Self) -> Self {
     BitSet64(self.0 & other.0)
 }

 // Count elements — popcount, hardware-accelerated
 fn len(&self) -> u32 {
     self.0.count_ones()
 }

 // Iterate over set bits
 fn iter(&self) -> impl Iterator<Item = u32> + '_ {
     (0u32..64).filter(move |&i| self.contains(i))
 }
}

// Usage
let mut a = BitSet64::new();
a.insert(0); a.insert(3); a.insert(7);

let mut b = BitSet64::new();
b.insert(3); b.insert(5); b.insert(7);

let both = a.intersection(&b);
for elem in both.iter() {
 println!("{elem}"); // 3, 7
}

// Flags pattern — named bits via constants
const FLAG_READ:  u32 = 0;
const FLAG_WRITE: u32 = 1;
const FLAG_EXEC:  u32 = 2;

let mut perms = BitSet64::new();
perms.insert(FLAG_READ);
perms.insert(FLAG_WRITE);
println!("can exec: {}", perms.contains(FLAG_EXEC)); // false

What This Unlocks

Key Differences

ConceptOCamlRust
Dense boolean set`Bytes` / `Bigarray``u64` bitmask or `[u64; N]`
Membership testarray index + comparebitwise AND (`mask & (1 << i)`)
Set unionloop + ORbitwise OR (`a \b`)
Set intersectionloop + ANDbitwise AND (`a & b`)
Element countmanual loop`.count_ones()` (hardware popcount)
Arbitrary size`Bigarray``bitvec` crate or `Vec<u64>`
#[derive(Clone, Copy, Debug, PartialEq)]
struct BitSet64(u64);

impl BitSet64 {
    fn empty() -> Self { Self(0) }
    fn with_capacity(_: usize) -> Self { Self(0) }
    fn insert(&mut self, i: u32) { assert!(i<64); self.0 |= 1u64 << i; }
    fn remove(&mut self, i: u32) { if i<64 { self.0 &= !(1u64 << i); } }
    fn contains(&self, i: u32) -> bool { i<64 && (self.0>>i)&1==1 }
    fn union(&self, other: &Self) -> Self { Self(self.0|other.0) }
    fn intersection(&self, other: &Self) -> Self { Self(self.0&other.0) }
    fn difference(&self, other: &Self) -> Self { Self(self.0 & !other.0) }
    fn count(&self) -> u32 { self.0.count_ones() }
    fn to_vec(&self) -> Vec<u32> { (0..64).filter(|&i| self.contains(i)).collect() }
    fn is_empty(&self) -> bool { self.0==0 }
}

// Large bitset using Vec<u64> blocks
struct BitSetN { data: Vec<u64>, capacity: usize }

impl BitSetN {
    fn new(capacity: usize) -> Self {
        Self { data: vec![0; (capacity+63)/64], capacity }
    }
    fn insert(&mut self, i: usize) { if i<self.capacity { self.data[i/64]|=1u64<<(i%64); } }
    fn contains(&self, i: usize) -> bool { i<self.capacity && (self.data[i/64]>>(i%64))&1==1 }
    fn count(&self) -> u32 { self.data.iter().map(|b|b.count_ones()).sum() }
}

fn main() {
    let mut a = BitSet64::empty();
    for i in [0u32,1,3,5,7] { a.insert(i); }
    let mut b = BitSet64::empty();
    for i in [1u32,2,3,4,5] { b.insert(i); }

    println!("A: {:?}", a.to_vec());
    println!("B: {:?}", b.to_vec());
    println!("A|B: {:?}", a.union(&b).to_vec());
    println!("A&B: {:?}", a.intersection(&b).to_vec());
    println!("A\B: {:?}", a.difference(&b).to_vec());

    // Large bitset
    let mut bs = BitSetN::new(256);
    for i in (0..256).step_by(7) { bs.insert(i); }
    println!("Large bitset count: {}", bs.count());
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test] fn insert_contains() { let mut b=BitSet64::empty(); b.insert(5); assert!(b.contains(5)); assert!(!b.contains(4)); }
    #[test] fn union_intersection() {
        let mut a=BitSet64::empty(); let mut b=BitSet64::empty();
        for i in [1u32,2,3] { a.insert(i); } for i in [2u32,3,4] { b.insert(i); }
        assert_eq!(a.intersection(&b).to_vec(), vec![2,3]);
    }
    #[test] fn count() { let mut b=BitSet64::empty(); for i in 0u32..10{b.insert(i);} assert_eq!(b.count(),10); }
}
(* OCaml: bitset via int (up to 62 bits on 64-bit) *)

let empty = 0
let add s i = s lor (1 lsl i)
let remove s i = s land (lnot (1 lsl i))
let mem s i = (s lsr i) land 1 = 1
let union = (lor)
let inter = (land)
let diff a b = a land (lnot b)
let to_list s = List.init 62 (fun i -> if mem s i then [i] else []) |> List.concat

let () =
  let a = List.fold_left add empty [0;1;3;5;7] in
  let b = List.fold_left add empty [1;2;3;4;5] in
  Printf.printf "A: %s\n" (String.concat "," (List.map string_of_int (to_list a)));
  Printf.printf "A∩B: %s\n" (String.concat "," (List.map string_of_int (to_list (inter a b))))