ExamplesBy LevelBy TopicLearning Paths
006 Fundamental

006 — Palindrome Check

Functional Programming

Tutorial

The Problem

A palindrome reads the same forwards and backwards — "racecar", "level", "A man a plan a canal Panama". Palindrome detection is a classic string processing problem that exercises iterator bidirectionality: comparing a sequence against its reverse without necessarily materializing the reverse. It appears in bioinformatics (DNA palindromes are recognition sites for restriction enzymes), signal processing (palindromic sequences), and string interview problems.

The problem also illustrates an important distinction between eager (allocating the reversed string) and lazy (comparing iterators without allocation) approaches — a recurring theme in functional programming where correctness comes first and optimization second.

🎯 Learning Outcomes

  • • Compare a sequence against its reverse using iterator bidirectionality
  • • Distinguish between allocating (collect to String) and non-allocating (.eq(rev())) approaches
  • • Handle Unicode correctly using chars() rather than byte-indexing
  • • Normalize input (lowercase, filter non-alphanumeric) for real-world palindrome checks
  • • Use Rust's DoubleEndedIterator for O(1) rev() without allocation
  • • Use s.chars().eq(s.chars().rev()) for zero-allocation palindrome checking that leverages DoubleEndedIterator
  • • Handle Unicode correctly with chars() rather than byte-level comparison
  • Code Example

    #![allow(clippy::all)]
    // 006: Palindrome Check
    
    // Approach 1: Reverse and compare (allocating)
    fn is_palindrome_rev(s: &str) -> bool {
        let reversed: String = s.chars().rev().collect();
        s == reversed
    }
    
    // Approach 2: Iterator comparison (zero allocation)
    fn is_palindrome_iter(s: &str) -> bool {
        s.chars().eq(s.chars().rev())
    }
    
    // Approach 3: Case-insensitive, alphanumeric only
    fn is_palindrome_clean(s: &str) -> bool {
        let clean: Vec<char> = s
            .chars()
            .filter(|c| c.is_alphanumeric())
            .map(|c| c.to_lowercase().next().unwrap())
            .collect();
        clean.iter().eq(clean.iter().rev())
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_palindrome_rev() {
            assert!(is_palindrome_rev("racecar"));
            assert!(is_palindrome_rev("abba"));
            assert!(!is_palindrome_rev("hello"));
            assert!(is_palindrome_rev(""));
            assert!(is_palindrome_rev("a"));
        }
    
        #[test]
        fn test_palindrome_iter() {
            assert!(is_palindrome_iter("racecar"));
            assert!(!is_palindrome_iter("abc"));
        }
    
        #[test]
        fn test_palindrome_clean() {
            assert!(is_palindrome_clean("A man, a plan, a canal: Panama"));
            assert!(is_palindrome_clean("Race Car"));
            assert!(!is_palindrome_clean("hello world"));
        }
    }

    Key Differences

  • Zero-copy rev: Rust's DoubleEndedIterator allows rev() on any iterator without allocation. OCaml's String.rev allocates a new string; you need a custom approach for zero-copy comparison.
  • Unicode: Rust's .chars() correctly iterates Unicode scalar values. Naive byte indexing (s.as_bytes()) breaks on multibyte characters. OCaml's s.[i] is byte-level by default; use Uchar for Unicode.
  • Normalization: Both languages require explicit lowercasing and filtering. Rust's char::to_lowercase() returns an iterator (handling ligatures that expand to multiple chars). OCaml's Char.lowercase_ascii is simpler but ASCII-only.
  • Comparison: Rust's iterator .eq() short-circuits at the first difference. OCaml's = on lists is structural equality that traverses the full list.
  • **DoubleEndedIterator:** Rust's .rev() on Chars works because Chars implements DoubleEndedIterator — it can be read from both ends. OCaml strings lack this; reversing requires String.init or a manual loop.
  • Unicode correctness: s.chars() iterates Unicode scalar values (code points). Byte-level reversal (s.bytes().rev()) would corrupt multi-byte characters. OCaml's strings are byte sequences — Unicode handling requires separate libraries.
  • Zero allocation: s.chars().eq(s.chars().rev()) allocates nothing — it compares lazily. OCaml's s = String.init (String.length s) (fun i -> s.[String.length s - 1 - i]) allocates a new string.
  • Normalization: Real-world palindrome checks require lowercasing and stripping non-alphanumeric characters. The to_lowercase() method in Rust returns an iterator (some Unicode characters expand to multiple chars), so .next().unwrap() is needed for single-char values.
  • OCaml Approach

    OCaml's String module lacks a built-in String.rev, but it is easily constructed: let rev s = String.init (String.length s) (fun i -> s.[String.length s - 1 - i]). Character-level access is s.[i]. For the clean version, OCaml uses String.to_seq and Seq.filter, then compares sequences. The functional style avoids mutation: build a clean char list, compare it with its reverse using List.for_all2.

    Full Source

    #![allow(clippy::all)]
    // 006: Palindrome Check
    
    // Approach 1: Reverse and compare (allocating)
    fn is_palindrome_rev(s: &str) -> bool {
        let reversed: String = s.chars().rev().collect();
        s == reversed
    }
    
    // Approach 2: Iterator comparison (zero allocation)
    fn is_palindrome_iter(s: &str) -> bool {
        s.chars().eq(s.chars().rev())
    }
    
    // Approach 3: Case-insensitive, alphanumeric only
    fn is_palindrome_clean(s: &str) -> bool {
        let clean: Vec<char> = s
            .chars()
            .filter(|c| c.is_alphanumeric())
            .map(|c| c.to_lowercase().next().unwrap())
            .collect();
        clean.iter().eq(clean.iter().rev())
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_palindrome_rev() {
            assert!(is_palindrome_rev("racecar"));
            assert!(is_palindrome_rev("abba"));
            assert!(!is_palindrome_rev("hello"));
            assert!(is_palindrome_rev(""));
            assert!(is_palindrome_rev("a"));
        }
    
        #[test]
        fn test_palindrome_iter() {
            assert!(is_palindrome_iter("racecar"));
            assert!(!is_palindrome_iter("abc"));
        }
    
        #[test]
        fn test_palindrome_clean() {
            assert!(is_palindrome_clean("A man, a plan, a canal: Panama"));
            assert!(is_palindrome_clean("Race Car"));
            assert!(!is_palindrome_clean("hello world"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_palindrome_rev() {
            assert!(is_palindrome_rev("racecar"));
            assert!(is_palindrome_rev("abba"));
            assert!(!is_palindrome_rev("hello"));
            assert!(is_palindrome_rev(""));
            assert!(is_palindrome_rev("a"));
        }
    
        #[test]
        fn test_palindrome_iter() {
            assert!(is_palindrome_iter("racecar"));
            assert!(!is_palindrome_iter("abc"));
        }
    
        #[test]
        fn test_palindrome_clean() {
            assert!(is_palindrome_clean("A man, a plan, a canal: Panama"));
            assert!(is_palindrome_clean("Race Car"));
            assert!(!is_palindrome_clean("hello world"));
        }
    }

    Deep Comparison

    Core Insight

    A palindrome reads the same forwards and backwards. The simplest check: reverse and compare. Both languages make this a one-liner, but the underlying data structures differ (OCaml string vs Rust UTF-8 String).

    OCaml Approach

  • • Convert string to char list, reverse, compare
  • • Or use index-based comparison from both ends
  • String.to_seq + List.of_seq for char extraction
  • Rust Approach

  • s.chars().rev().collect::<String>() == s
  • • Or s.chars().eq(s.chars().rev()) — no allocation
  • .chars() handles UTF-8 correctly
  • Comparison Table

    FeatureOCamlRust
    Reverse stringString.to_seq \|> List.of_seq \|> List.revs.chars().rev()
    Compare= structural equality== or .eq()
    UTF-8Byte-based strings.chars() for Unicode
    Zero-alloc checkIndex loop.chars().eq(s.chars().rev())

    Exercises

  • Largest palindrome substring: Write largest_palindrome(s: &str) -> &str that returns the longest palindromic substring using Manacher's algorithm or naive O(n²) expansion.
  • Palindrome pairs: Given a list of words, find all pairs (i, j) where concatenating words i and j forms a palindrome.
  • Streaming check: Write is_palindrome_stream(iter: impl Iterator<Item=char>) -> bool that checks if a character stream is a palindrome by collecting to Vec<char> then comparing with its reverse.
  • Longest palindrome substring: Implement Manacher's algorithm (or a simpler O(n²) version) to find the longest palindromic substring of a given string.
  • Palindrome pairs: Given a list of words, find all pairs (i, j) where words[i] + words[j] is a palindrome, using HashSet for O(n) lookup.
  • Open Source Repos