Hamming Distance — Generic Zip
Tutorial Video
Text description (accessibility)
This video demonstrates the "Hamming Distance — Generic Zip" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Higher-Order Functions. Compute the Hamming distance between two strings: the number of positions where corresponding characters differ. Key difference from OCaml: 1. **Result type:** OCaml's `Ok/Error` maps directly to Rust's `Ok/Err` — nearly identical usage
Tutorial
The Problem
Compute the Hamming distance between two strings: the number of positions where corresponding characters differ. Return an error if the strings have different lengths.
🎯 Learning Outcomes
Result<T, E> for fallible computations (mirrors OCaml's result type)zip + filter + count as a clean functional pipelinemut counter) vs functional (fold/filter) styles? operator vs explicit match🦀 The Rust Way
Rust's idiomatic version chains .chars().zip().filter().count() — no mutable state needed. The Result type maps directly to OCaml's result. An imperative version with mut dist and a fold version are also shown for comparison.
Code Example
pub fn hamming(s1: &str, s2: &str) -> Result<usize, &'static str> {
if s1.len() != s2.len() {
return Err("strands must be of equal length");
}
Ok(s1.chars().zip(s2.chars()).filter(|(a, b)| a != b).count())
}Key Differences
Ok/Error maps directly to Rust's Ok/Err — nearly identical usageSeq.zip and Rust's Iterator::zip are functionally equivalent; Rust's is a method on any iteratorString.iteri (index + char) or String.to_seq; Rust uses .chars() which returns a char iterator directly.filter(predicate).count() is more readable than .fold(0, |acc, ...| ...) for counting — both work, but filter+count communicates intent betterOCaml Approach
OCaml offers two styles: an imperative version using ref (mutable reference) with String.iteri, and a pure functional version using Seq.zip with Seq.fold_left. Both return result (Ok/Error) for the length check.
Full Source
#![allow(clippy::all)]
/// Compute the Hamming distance between two strings of equal length.
///
/// The Hamming distance is the number of positions where the corresponding
/// characters differ. Returns an error if the strings have different lengths.
///
/// # Idiomatic Rust — zip + filter + count
pub fn hamming(s1: &str, s2: &str) -> Result<usize, &'static str> {
if s1.len() != s2.len() {
return Err("strands must be of equal length");
}
Ok(s1.chars().zip(s2.chars()).filter(|(a, b)| a != b).count())
}
/// Imperative version — closer to OCaml's ref-based approach.
pub fn hamming_imperative(s1: &str, s2: &str) -> Result<usize, &'static str> {
if s1.len() != s2.len() {
return Err("strands must be of equal length");
}
let mut dist = 0;
for (a, b) in s1.chars().zip(s2.chars()) {
if a != b {
dist += 1;
}
}
Ok(dist)
}
/// Fold-based version — functional accumulator pattern.
pub fn hamming_fold(s1: &str, s2: &str) -> Result<usize, &'static str> {
if s1.len() != s2.len() {
return Err("strands must be of equal length");
}
Ok(s1
.chars()
.zip(s2.chars())
.fold(0, |acc, (a, b)| if a != b { acc + 1 } else { acc }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_strands() {
assert_eq!(hamming("", ""), Ok(0));
}
#[test]
fn test_identical_strands() {
assert_eq!(hamming("GGACTGA", "GGACTGA"), Ok(0));
}
#[test]
fn test_single_difference() {
assert_eq!(hamming("GGACTGA", "GGACTGT"), Ok(1));
}
#[test]
fn test_long_strands() {
assert_eq!(hamming("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT"), Ok(7));
}
#[test]
fn test_unequal_length() {
assert_eq!(hamming("AB", "ABC"), Err("strands must be of equal length"));
}
#[test]
fn test_all_different() {
assert_eq!(hamming("ABCD", "EFGH"), Ok(4));
}
#[test]
fn test_imperative_matches() {
assert_eq!(
hamming("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT"),
hamming_imperative("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT")
);
}
#[test]
fn test_fold_matches() {
assert_eq!(
hamming("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT"),
hamming_fold("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT")
);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_strands() {
assert_eq!(hamming("", ""), Ok(0));
}
#[test]
fn test_identical_strands() {
assert_eq!(hamming("GGACTGA", "GGACTGA"), Ok(0));
}
#[test]
fn test_single_difference() {
assert_eq!(hamming("GGACTGA", "GGACTGT"), Ok(1));
}
#[test]
fn test_long_strands() {
assert_eq!(hamming("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT"), Ok(7));
}
#[test]
fn test_unequal_length() {
assert_eq!(hamming("AB", "ABC"), Err("strands must be of equal length"));
}
#[test]
fn test_all_different() {
assert_eq!(hamming("ABCD", "EFGH"), Ok(4));
}
#[test]
fn test_imperative_matches() {
assert_eq!(
hamming("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT"),
hamming_imperative("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT")
);
}
#[test]
fn test_fold_matches() {
assert_eq!(
hamming("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT"),
hamming_fold("GAGCCTACTAACGGGAT", "CATCGTAATGACGGCCT")
);
}
}
Deep Comparison
OCaml vs Rust: Hamming Distance
Side-by-Side Code
OCaml
(* Imperative version *)
let hamming s1 s2 =
if String.length s1 <> String.length s2 then
Error "strands must be of equal length"
else
let dist = ref 0 in
String.iteri (fun i c ->
if c <> s2.[i] then incr dist
) s1;
Ok !dist
(* Pure functional version *)
let hamming_fp s1 s2 =
if String.length s1 <> String.length s2 then Error "unequal"
else
Ok (Seq.zip (String.to_seq s1) (String.to_seq s2)
|> Seq.fold_left (fun acc (a, b) -> if a <> b then acc + 1 else acc) 0)
Rust (idiomatic)
pub fn hamming(s1: &str, s2: &str) -> Result<usize, &'static str> {
if s1.len() != s2.len() {
return Err("strands must be of equal length");
}
Ok(s1.chars().zip(s2.chars()).filter(|(a, b)| a != b).count())
}
Rust (functional/fold)
pub fn hamming_fold(s1: &str, s2: &str) -> Result<usize, &'static str> {
if s1.len() != s2.len() {
return Err("strands must be of equal length");
}
Ok(s1.chars().zip(s2.chars())
.fold(0, |acc, (a, b)| if a != b { acc + 1 } else { acc }))
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Function signature | val hamming : string -> string -> (int, string) result | fn hamming(s1: &str, s2: &str) -> Result<usize, &'static str> |
| Success | Ok 7 | Ok(7) |
| Error | Error "message" | Err("message") |
| Mutable counter | let dist = ref 0 ... incr dist ... !dist | let mut dist = 0 ... dist += 1 |
| Char comparison | c <> s2.[i] (index access) | a != b (via zip, no indexing) |
Key Insights
(int, string) result with Ok/Error maps directly to Rust's Result<usize, &str> with Ok/Err — the error handling pattern is the same in both languagesString.iteri with index i and s2.[i]; Rust's zip pairs characters automatically — no index needed, no bounds-check concerns.filter(pred).count() is more declarative than .fold(0, |acc, x| if pred(x) { acc + 1 } else { acc }) — it communicates "count matching items" directlyref vs mut:** OCaml's ref creates a heap-allocated mutable cell; Rust's mut is a stack variable — both allow mutation in an otherwise functional style, but Rust's is zero-costString.length and Rust's str::len() are both O(1) for byte length (but note: Rust's .chars().count() would be O(n) for char count)When to Use Each Style
Use idiomatic Rust when: You want the clearest, most concise code — zip().filter().count() is a single pipeline that reads like English: "zip the characters, filter where they differ, count the differences."
Use fold Rust when: You need to accumulate something more complex than a count — fold generalizes to any accumulator. Here it's overkill, but the pattern is worth knowing for more complex reductions.
Exercises
hamming_distance to work on any two Iterator<Item: PartialEq> of the same length, returning Err if lengths differ.nearest_neighbor that takes a query sequence and a list of candidates, returning the candidate with the smallest Hamming distance.