๐Ÿฆ€ 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

093: Isogram Check

Difficulty: 1 Level: Beginner Check whether a word contains no repeated letters (ignoring case, hyphens, and spaces).

The Problem This Solves

"Lumberjacks" is an isogram โ€” every letter appears exactly once. "Eleven" is not โ€” `e` appears three times. Isogram checking is a clean exercise in duplicate detection: the same pattern appears in finding duplicate IDs, validating unique usernames, checking for repeated elements in configuration. Three approaches with different trade-offs: sort-and-dedup (mirrors OCaml's `List.sort_uniq`, extra memory), hash set with early exit (most practical), bitset (fastest for ASCII alphabets).

The Intuition

The set approach: as you scan letters, add each to a set. If a letter is already in the set, you found a duplicate โ€” it's not an isogram. If you reach the end without duplicates, it is an isogram. `HashSet::insert` in Rust returns `false` when the element was already present. Combined with `.all()`, this gives early exit on the first duplicate without any explicit `if` check. The bitset approach: same idea but with a `u32`. If the bit for a letter is already set when you encounter it, you found a duplicate. Otherwise, set the bit and continue.

How It Works in Rust

use std::collections::HashSet;

// HashSet with early exit โ€” idiomatic, practical
pub fn is_isogram_hashset(s: &str) -> bool {
 let mut seen = HashSet::new();
 s.chars()
     .filter(|c| c.is_ascii_alphabetic())  // ignore hyphens, spaces
     .all(|c| seen.insert(c.to_ascii_lowercase()))
     // insert returns false if already present โ†’ .all() short-circuits
}

// Sort + dedup โ€” mirrors OCaml's List.sort_uniq
pub fn is_isogram_sort(s: &str) -> bool {
 let mut chars: Vec<char> = s.chars()
     .filter_map(|c| c.is_ascii_alphabetic().then_some(c.to_ascii_lowercase()))
     .collect();
 let total = chars.len();
 chars.sort_unstable();
 chars.dedup();          // remove consecutive duplicates (works after sort)
 chars.len() == total    // if dedup removed anything, there were duplicates
}

// Bitset โ€” fastest for ASCII letters
pub fn is_isogram_bitset(s: &str) -> bool {
 let mut bits: u32 = 0;
 for c in s.chars().filter(|c| c.is_ascii_alphabetic()) {
     let mask = 1 << (c.to_ascii_lowercase() as u32 - 'a' as u32);
     if bits & mask != 0 { return false; }  // bit already set = duplicate
     bits |= mask;
 }
 true
}
The `HashSet::insert` returning `bool` trick is the most idiomatic Rust: it makes `.all()` the natural way to check "no duplicates found while inserting."

What This Unlocks

Key Differences

ConceptOCamlRust
Sort + dedup`List.sort_uniq Char.compare chars` (one call)`sort_unstable()` then `dedup()` (two calls)
HashSet duplicate check`not (List.mem c seen)` + add`seen.insert(c)` returns `false` on duplicate
Early exit`List.exists` / exception`.all(\c\seen.insert(c))` short-circuits naturally
Letter filter`Char.code c >= 97 && ...``.is_ascii_alphabetic()`
BitsetManual bit ops`u32` with `&` and `\=`
//! # Isogram Check
//!
//! A word is an isogram if no letter repeats. Compares sort-based,
//! HashSet-based, and bitset approaches.

use std::collections::HashSet;

// ---------------------------------------------------------------------------
// Approach A: Sort + dedup (mirrors OCaml's List.sort_uniq)
// ---------------------------------------------------------------------------

pub fn is_isogram_sort(s: &str) -> bool {
    let mut chars: Vec<char> = s
        .chars()
        .filter_map(|c| {
            let lc = c.to_ascii_lowercase();
            lc.is_ascii_lowercase().then_some(lc)
        })
        .collect();
    let total = chars.len();
    chars.sort_unstable();
    chars.dedup();
    chars.len() == total
}

// ---------------------------------------------------------------------------
// Approach B: HashSet โ€” insert returns false on duplicate
// ---------------------------------------------------------------------------

pub fn is_isogram_hashset(s: &str) -> bool {
    let mut seen = HashSet::new();
    s.chars()
        .filter(|c| c.is_ascii_alphabetic())
        .all(|c| seen.insert(c.to_ascii_lowercase()))
}

// ---------------------------------------------------------------------------
// Approach C: Bitset
// ---------------------------------------------------------------------------

pub fn is_isogram_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() {
            let mask = 1 << (lc as u32 - 'a' as u32);
            if bits & mask != 0 {
                return false;
            }
            bits |= mask;
        }
    }
    true
}

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

    #[test]
    fn test_isogram() {
        assert!(is_isogram_sort("lumberjacks"));
        assert!(is_isogram_hashset("lumberjacks"));
        assert!(is_isogram_bitset("lumberjacks"));
    }

    #[test]
    fn test_not_isogram() {
        assert!(!is_isogram_sort("eleven"));
        assert!(!is_isogram_hashset("eleven"));
        assert!(!is_isogram_bitset("eleven"));
    }

    #[test]
    fn test_long_isogram() {
        assert!(is_isogram_bitset("subdermatoglyphic"));
    }

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

    #[test]
    fn test_with_spaces() {
        assert!(is_isogram_hashset("big dwarf"));
    }
}

fn main() {
    println!("{:?}", is_isogram_sort("lumberjacks"));
    println!("{:?}", is_isogram_hashset("lumberjacks"));
    println!("{:?}", is_isogram_bitset("lumberjacks"));
}
let is_isogram s =
  let chars = s |> String.lowercase_ascii |> String.to_seq
    |> Seq.filter (fun c -> c >= 'a' && c <= 'z')
    |> List.of_seq in
  let unique = List.sort_uniq Char.compare chars in
  List.length chars = List.length unique

let () =
  List.iter (fun s ->
    Printf.printf "%s: %b\n" s (is_isogram s)
  ) ["lumberjacks"; "background"; "eleven"; "subdermatoglyphic"]

๐Ÿ“Š Detailed Comparison

Comparison: Isogram Check โ€” OCaml vs Rust

Core Insight

OCaml's `List.sort_uniq` elegantly combines sorting and deduplication. Rust separates these operations but offers a more powerful alternative: `HashSet::insert` returns a boolean indicating whether the element was new, allowing early termination on the first duplicate โ€” something OCaml's approach cannot do.

OCaml

๐Ÿช Show OCaml equivalent
let is_isogram s =
let chars = s |> String.lowercase_ascii |> String.to_seq
 |> Seq.filter (fun c -> c >= 'a' && c <= 'z') |> List.of_seq in
let unique = List.sort_uniq Char.compare chars in
List.length chars = List.length unique

Rust โ€” HashSet with early exit

pub fn is_isogram_hashset(s: &str) -> bool {
 let mut seen = HashSet::new();
 s.chars()
     .filter(|c| c.is_ascii_alphabetic())
     .all(|c| seen.insert(c.to_ascii_lowercase()))
}

Comparison Table

AspectOCamlRust
Dedup`List.sort_uniq``sort_unstable()` + `dedup()`
Early exitNo (processes all)`HashSet::insert` + `all()`
Set approachWould need `Set.Make(Char)``HashSet<char>` built-in
Bitset`lor`/`lsl``\=`/`<<`
ComplexityO(n log n)O(n) with HashSet/bitset

Learner Notes

  • `HashSet::insert` idiom: Returning bool on insert is uniquely useful โ€” OCaml sets don't offer this
  • `all()` short-circuits: Stops at first `false`, making this O(k) where k is first duplicate position
  • Bitset is fastest: For ASCII-only, 26 bits in a `u32` beats any collection
  • `sort_unstable`: Rust's unstable sort doesn't preserve order of equal elements but is faster โ€” fine here since we just dedup