ExamplesBy LevelBy TopicLearning Paths
092 Fundamental

092 — Pangram Check

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "092 — Pangram Check" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Determine whether a string contains every letter of the alphabet at least once. Key difference from OCaml: | Aspect | Rust HashSet | Rust Bitset | OCaml |

Tutorial

The Problem

Determine whether a string contains every letter of the alphabet at least once. Implement three approaches: HashSet-based (collect lowercase letters, check .len() == 26), bitset-based (OR 26 bits, check all set), and functional (check ('a'..='z').all(|c| lower.contains(c))). Compare with OCaml's Set.Make(Char) and subset check.

🎯 Learning Outcomes

  • • Collect characters into a HashSet<char> and check cardinality
  • • Use a u32 bitset with bit-shifting to track 26 booleans in one integer
  • • Apply ('a'..='z').all(|c| s.contains(c)) for a readable functional check
  • • Use filter_map to normalise case and filter non-alpha in one pass
  • • Map Rust's three approaches to OCaml's CS.subset alphabet chars
  • • Choose the right approach based on readability vs performance requirements
  • Code Example

    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
    }

    Key Differences

    AspectRust HashSetRust BitsetOCaml
    Data structureHashSet<char>u32Set.Make(Char)
    Check.len() == 26bits == (1<<26)-1CS.subset
    Memory~26 entries4 bytes~26 balanced nodes
    PerformanceO(n) avgO(n) optimalO(n log 26)
    ReadabilityHighLowHigh
    Case normaliseto_ascii_lowercaseSameString.lowercase_ascii

    For a pangram check, the bitset is the fastest (single integer comparison) but least readable. The all approach is most readable but least efficient. In practice, the bitset version is preferred when performance matters; the HashSet version when generality does.

    OCaml Approach

    OCaml builds the alphabet set once as a CS.of_list of 26 chars. is_pangram filters the input string to lowercase letters via Seq.filter, builds a CS.of_seq, then checks CS.subset alphabet chars. This is clean and declarative. OCaml's Set.Make(Char) is a balanced BST, so operations are O(log n); Rust's HashSet is O(1) average.

    Full Source

    #![allow(clippy::all)]
    //! # 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"
            ));
        }
    }
    ✓ Tests Rust test suite
    #[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"
            ));
        }
    }

    Deep 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

    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 creationSet.Make(Char) functorHashSet<char> directly
    Subset checkCS.subset alphabet charschars.len() == 26
    LowercaseString.lowercase_ascii.to_ascii_lowercase()
    FilteringSeq.filter.filter_map()
    Bitset approachManual with lor/lslSame 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
  • Exercises

  • Add a missing_letters(s: &str) -> Vec<char> function that returns letters not present in the string.
  • Implement a pangram_score(s: &str) -> usize that counts how many distinct letters appear.
  • Write a property test: is_pangram_hashset(s) == is_pangram_bitset(s) == is_pangram_all(s) for any s.
  • Extend to Unicode: check whether a string contains all letters from a given alphabet set (not just ASCII).
  • In OCaml, benchmark CS.subset-based vs bitset-based pangram check on a 1MB string with all letters.
  • Open Source Repos