ExamplesBy LevelBy TopicLearning Paths
280 Fundamental

Hamming Distance — Generic Zip

Higher-Order Functions

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

  • • Using Result<T, E> for fallible computations (mirrors OCaml's result type)
  • • Iterator zip + filter + count as a clean functional pipeline
  • • Comparing imperative (mut counter) vs functional (fold/filter) styles
  • • Early return with ? 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

  • Result type: OCaml's Ok/Error maps directly to Rust's Ok/Err — nearly identical usage
  • Zip: OCaml's Seq.zip and Rust's Iterator::zip are functionally equivalent; Rust's is a method on any iterator
  • String iteration: OCaml uses String.iteri (index + char) or String.to_seq; Rust uses .chars() which returns a char iterator directly
  • Filter+count vs fold: Rust's .filter(predicate).count() is more readable than .fold(0, |acc, ...| ...) for counting — both work, but filter+count communicates intent better
  • OCaml 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")
            );
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Function signatureval hamming : string -> string -> (int, string) resultfn hamming(s1: &str, s2: &str) -> Result<usize, &'static str>
    SuccessOk 7Ok(7)
    ErrorError "message"Err("message")
    Mutable counterlet dist = ref 0 ... incr dist ... !distlet mut dist = 0 ... dist += 1
    Char comparisonc <> s2.[i] (index access)a != b (via zip, no indexing)

    Key Insights

  • Result type is almost identical: OCaml's (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 languages
  • Zip eliminates indexing: OCaml's imperative version uses String.iteri with index i and s2.[i]; Rust's zip pairs characters automatically — no index needed, no bounds-check concerns
  • Filter+count > fold for counting: Rust's .filter(pred).count() is more declarative than .fold(0, |acc, x| if pred(x) { acc + 1 } else { acc }) — it communicates "count matching items" directly
  • **ref 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-cost
  • String length check: Both languages check length upfront and return early on mismatch. OCaml's String.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

  • Generalize hamming_distance to work on any two Iterator<Item: PartialEq> of the same length, returning Err if lengths differ.
  • Implement nearest_neighbor that takes a query sequence and a list of candidates, returning the candidate with the smallest Hamming distance.
  • Build a simple error-correcting code using Hamming(7,4): encode a 4-bit data word into a 7-bit codeword, introduce a single-bit error, and implement the decoder that detects and corrects the error using parity checks.
  • Open Source Repos