๐Ÿฆ€ Functional Rust
๐ŸŽฌ How Rust Iterators Work Lazy evaluation, chaining, collect(), and zero-cost abstractions.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Iterators are lazy โ€” .map(), .filter(), .take() build a chain but do no work until consumed

โ€ข .collect() triggers evaluation, transforming the chain into a Vec, HashMap, or other collection

โ€ข Zero-cost abstraction: iterator chains compile to the same machine code as hand-written loops

โ€ข .iter() borrows, .into_iter() consumes, .iter_mut() borrows mutably

โ€ข Chaining replaces nested loops with a readable, composable pipeline

092: Pangram Check

Difficulty: 1 Level: Beginner Determine whether a string contains every letter of the English alphabet at least once.

The Problem This Solves

"The quick brown fox jumps over the lazy dog" is a famous pangram โ€” it contains all 26 letters. Pangram detection is a clean string-processing exercise that exercises character classification, set operations, and bit manipulation. In production, this pattern generalises to "does this input contain all required elements?" โ€” checking required fields, mandatory keywords, complete alphabets, all required keys in a config. The algorithms here scale from 26 letters to arbitrary required sets.

The Intuition

The naive approach: build a set of all lowercase letters in the string, then check if the set has 26 elements. That's the `HashSet` approach โ€” intuitive, O(n) time, O(26) space. A faster approach for fixed alphabets: use 26 bits of a `u32`. Bit `i` represents letter `i`. Set bit `i` when letter `i` appears. When all 26 bits are set, the number equals `(1 << 26) - 1` โ€” you have a pangram. No hash table, just integer operations. The most readable approach: `('a'..='z').all(|c| s.contains(c))`. Correct, O(26n) time, but the intent is crystal clear.

How It Works in Rust

use std::collections::HashSet;

// HashSet approach โ€” mirrors OCaml's Set.Make(Char)
pub fn is_pangram_hashset(s: &str) -> bool {
 let chars: HashSet<char> = s.chars()
     .filter_map(|c| {
         let lc = c.to_ascii_lowercase();
         if lc.is_ascii_lowercase() { Some(lc) } else { None }
     })
     .collect();
 chars.len() == 26  // exactly 26 distinct letters = pangram
}

// Bitset approach โ€” 26 bits, one per letter
pub fn is_pangram_bitset(s: &str) -> bool {
 let mut bits: u32 = 0;
 for c in s.chars() {
     let lc = c.to_ascii_lowercase();
     if lc.is_ascii_lowercase() {
         bits |= 1 << (lc as u32 - 'a' as u32);  // set bit for this letter
     }
 }
 bits == (1 << 26) - 1  // all 26 bits set?
}

// Declarative approach โ€” reads like the definition
pub fn is_pangram_all(s: &str) -> bool {
 let lower = s.to_ascii_lowercase();
 ('a'..='z').all(|c| lower.contains(c))
}
The bitset approach is the fastest in practice: integer comparison instead of hash lookups, cache-friendly, branchless bit operations.

What This Unlocks

Key Differences

ConceptOCamlRust
Char set`module CharSet = Set.Make(Char)` โ€” functor required`HashSet<char>` โ€” works out of the box
BitsetManual `Int32` manipulation`u32` with `\=` and `<<`
Lowercase`Char.lowercase_ascii``.to_ascii_lowercase()`
Letter check`Char.code c - Char.code 'a'``c as u32 - 'a' as u32`
All-letters check`CharSet.cardinal set = 26``chars.len() == 26` or `bits == (1<<26)-1`
//! # Pangram Check
//!
//! Determine if a string contains every letter of the alphabet.
//! Compares OCaml's `Set.Make(Char)` with Rust's `HashSet` and bitset approaches.

use std::collections::HashSet;

// ---------------------------------------------------------------------------
// Approach A: HashSet โ€” mirrors OCaml's Set approach
// ---------------------------------------------------------------------------

pub fn is_pangram_hashset(s: &str) -> bool {
    let chars: HashSet<char> = s
        .chars()
        .filter_map(|c| {
            let lc = c.to_ascii_lowercase();
            if lc.is_ascii_lowercase() { Some(lc) } else { None }
        })
        .collect();
    chars.len() == 26
}

// ---------------------------------------------------------------------------
// Approach B: Bitset โ€” 26 bits for 26 letters
// ---------------------------------------------------------------------------

pub fn is_pangram_bitset(s: &str) -> bool {
    let mut bits: u32 = 0;
    for c in s.chars() {
        let lc = c.to_ascii_lowercase();
        if lc.is_ascii_lowercase() {
            bits |= 1 << (lc as u32 - 'a' as u32);
        }
    }
    bits == (1 << 26) - 1
}

// ---------------------------------------------------------------------------
// Approach C: Functional โ€” all() check
// ---------------------------------------------------------------------------

pub fn is_pangram_all(s: &str) -> bool {
    let lower = s.to_ascii_lowercase();
    ('a'..='z').all(|c| lower.contains(c))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_pangram() {
        let s = "The quick brown fox jumps over the lazy dog";
        assert!(is_pangram_hashset(s));
        assert!(is_pangram_bitset(s));
        assert!(is_pangram_all(s));
    }

    #[test]
    fn test_not_pangram() {
        assert!(!is_pangram_hashset("Hello world"));
        assert!(!is_pangram_bitset("Hello world"));
        assert!(!is_pangram_all("Hello world"));
    }

    #[test]
    fn test_empty() {
        assert!(!is_pangram_hashset(""));
        assert!(!is_pangram_bitset(""));
    }

    #[test]
    fn test_with_numbers() {
        let s = "The 1 quick brown fox jumps over the 2 lazy dogs";
        assert!(is_pangram_hashset(s));
        assert!(is_pangram_bitset(s));
    }

    #[test]
    fn test_missing_one() {
        assert!(!is_pangram_bitset("The quick brown fox jumps over the lazy do"));
    }
}

fn main() {
    println!("{:?}", is_pangram_hashset(s));
    println!("{:?}", is_pangram_bitset(s));
    println!("{:?}", is_pangram_all(s));
}
module CS = Set.Make(Char)

let alphabet = 
  List.init 26 (fun i -> Char.chr (i + Char.code 'a'))
  |> CS.of_list

let is_pangram s =
  let chars = s |> String.lowercase_ascii |> String.to_seq
    |> Seq.filter (fun c -> c >= 'a' && c <= 'z')
    |> CS.of_seq in
  CS.subset alphabet chars

let () =
  Printf.printf "%b\n" (is_pangram "The quick brown fox jumps over the lazy dog");
  Printf.printf "%b\n" (is_pangram "Hello world")

๐Ÿ“Š Detailed Comparison

Comparison: Pangram Check โ€” OCaml vs Rust

Core Insight

OCaml models this with its functor system (`Set.Make(Char)`) โ€” you create a specialized set module for characters. Rust's generics mean `HashSet<char>` just works. The deeper lesson is about abstraction cost: OCaml's functors are more explicit and modular, while Rust's trait-based generics are more ergonomic for common cases.

OCaml

๐Ÿช Show OCaml equivalent
module CS = Set.Make(Char)
let alphabet = List.init 26 (fun i -> Char.chr (i + Char.code 'a')) |> CS.of_list
let is_pangram s =
let chars = s |> String.lowercase_ascii |> String.to_seq
 |> Seq.filter (fun c -> c >= 'a' && c <= 'z') |> CS.of_seq in
CS.subset alphabet chars

Rust โ€” HashSet

pub fn is_pangram_hashset(s: &str) -> bool {
 let chars: HashSet<char> = s.chars()
     .filter_map(|c| { let lc = c.to_ascii_lowercase(); lc.is_ascii_lowercase().then_some(lc) })
     .collect();
 chars.len() == 26
}

Rust โ€” Bitset

pub fn is_pangram_bitset(s: &str) -> bool {
 let mut bits: u32 = 0;
 for c in s.chars() {
     let lc = c.to_ascii_lowercase();
     if lc.is_ascii_lowercase() { bits |= 1 << (lc as u32 - 'a' as u32); }
 }
 bits == (1 << 26) - 1
}

Comparison Table

AspectOCamlRust
Set creation`Set.Make(Char)` functor`HashSet<char>` directly
Subset check`CS.subset alphabet chars``chars.len() == 26`
Lowercase`String.lowercase_ascii``.to_ascii_lowercase()`
Filtering`Seq.filter``.filter_map()`
Bitset approachManual with `lor`/`lsl`Same with `\=`/`<<`
PerformanceO(n log n) with balanced tree setO(n) with bitset

Learner Notes

  • Functor vs Generics: OCaml's `Set.Make` creates a module; Rust's `HashSet` uses trait bounds โ€” different abstraction styles
  • Bitset trick: Both languages support it; Rust's `u32` bit ops are identical to OCaml's `lor`/`lsl`
  • Early exit: The bitset approach can short-circuit once `bits == ALL` โ€” neither the OCaml nor basic Rust version does this
  • `filter_map`: Rust's combined filter+map avoids an intermediate allocation that separate filter + map would need