092 — Pangram Check
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
HashSet<char> and check cardinalityu32 bitset with bit-shifting to track 26 booleans in one integer('a'..='z').all(|c| s.contains(c)) for a readable functional checkfilter_map to normalise case and filter non-alpha in one passCS.subset alphabet charsCode 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
| Aspect | Rust HashSet | Rust Bitset | OCaml |
|---|---|---|---|
| Data structure | HashSet<char> | u32 | Set.Make(Char) |
| Check | .len() == 26 | bits == (1<<26)-1 | CS.subset |
| Memory | ~26 entries | 4 bytes | ~26 balanced nodes |
| Performance | O(n) avg | O(n) optimal | O(n log 26) |
| Readability | High | Low | High |
| Case normalise | to_ascii_lowercase | Same | String.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"
));
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| 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 approach | Manual with lor/lsl | Same with \|=/<< |
| Performance | O(n log n) with balanced tree set | O(n) with bitset |
Learner Notes
Set.Make creates a module; Rust's HashSet uses trait bounds — different abstraction stylesu32 bit ops are identical to OCaml's lor/lslbits == ALL — neither the OCaml nor basic Rust version does thisfilter_map**: Rust's combined filter+map avoids an intermediate allocation that separate filter + map would needExercises
missing_letters(s: &str) -> Vec<char> function that returns letters not present in the string.pangram_score(s: &str) -> usize that counts how many distinct letters appear.is_pangram_hashset(s) == is_pangram_bitset(s) == is_pangram_all(s) for any s.CS.subset-based vs bitset-based pangram check on a 1MB string with all letters.