006 — Palindrome Check
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
collect to String) and non-allocating (.eq(rev())) approacheschars() rather than byte-indexingDoubleEndedIterator for O(1) rev() without allocations.chars().eq(s.chars().rev()) for zero-allocation palindrome checking that leverages DoubleEndedIteratorchars() rather than byte-level comparisonCode 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
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..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.char::to_lowercase() returns an iterator (handling ligatures that expand to multiple chars). OCaml's Char.lowercase_ascii is simpler but ASCII-only..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.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.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.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"));
}
}#[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
String.to_seq + List.of_seq for char extractionRust Approach
s.chars().rev().collect::<String>() == ss.chars().eq(s.chars().rev()) — no allocation.chars() handles UTF-8 correctlyComparison Table
| Feature | OCaml | Rust |
|---|---|---|
| Reverse string | String.to_seq \|> List.of_seq \|> List.rev | s.chars().rev() |
| Compare | = structural equality | == or .eq() |
| UTF-8 | Byte-based strings | .chars() for Unicode |
| Zero-alloc check | Index loop | .chars().eq(s.chars().rev()) |
Exercises
largest_palindrome(s: &str) -> &str that returns the longest palindromic substring using Manacher's algorithm or naive O(n²) expansion.(i, j) where concatenating words i and j forms a palindrome.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.(i, j) where words[i] + words[j] is a palindrome, using HashSet for O(n) lookup.