ExamplesBy LevelBy TopicLearning Paths
091 Fundamental

091 — Caesar Cipher

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "091 — Caesar Cipher" functional Rust example. Difficulty level: Fundamental. Key concepts covered: Functional Programming. Implement the Caesar cipher: rotate each alphabetic character by `n` positions, wrapping around with modular arithmetic (`% 26`). Key difference from OCaml: | Aspect | Rust | OCaml |

Tutorial

The Problem

Implement the Caesar cipher: rotate each alphabetic character by n positions, wrapping around with modular arithmetic (% 26). Preserve non-alphabetic characters unchanged. Implement three versions: iterator-based, fold-based, and in-place byte mutation — and compare with OCaml's single-pass String.map approach.

🎯 Learning Outcomes

  • • Use byte arithmetic (c as u8 - b'a' + n) % 26 for letter rotation
  • • Apply s.chars().map(shift_char).collect::<String>() for idiomatic transformation
  • • Use fold with String::with_capacity to avoid repeated reallocation
  • • Understand the in-place byte mutation approach and when it is safe (ASCII only)
  • • Map Rust's .chars().map().collect() to OCaml's String.map f s
  • • Recognise that decrypt(n) is just caesar(26 - n) — a clean mathematical inverse
  • Code Example

    pub fn caesar(n: u8, s: &str) -> String {
        s.chars().map(|c| shift_char(n, c)).collect()
    }

    Key Differences

    AspectRustOCaml
    Map over string.chars().map(f).collect()String.map f s
    Char to intc as u8Char.code c
    Int to charx as charChar.chr x
    Byte rangesb'a'..=b'z'c >= 'a' && c <= 'z'
    In-placeVec<u8> + from_utf8Not idiomatic
    Decryptcaesar(26 - n)caesar (26 - n)

    The three Rust implementations cover three idioms: functional map/collect, fold with pre-allocated buffer, and low-level byte mutation. In practice, the iterator version is the most idiomatic; the byte version is only useful when profiling confirms allocation overhead is significant.

    OCaml Approach

    OCaml's String.map (shift_char n) s applies the character function uniformly. shift_char uses Char.code and Char.chr for arithmetic on character codes. The decryption is caesar (26 - n). The code is more concise because String.map is a one-liner abstraction and Char.code/Char.chr avoid manual casting.

    Full Source

    #![allow(clippy::all)]
    //! # Caesar Cipher — Functional Encryption
    //!
    //! Character-level string transformation using modular arithmetic.
    //! OCaml's `String.map` maps to Rust's iterator `.map().collect()`.
    
    // ---------------------------------------------------------------------------
    // Approach A: Idiomatic Rust — iterators + collect
    // ---------------------------------------------------------------------------
    
    pub fn shift_char(n: u8, c: char) -> char {
        match c {
            'a'..='z' => (b'a' + (c as u8 - b'a' + n) % 26) as char,
            'A'..='Z' => (b'A' + (c as u8 - b'A' + n) % 26) as char,
            _ => c,
        }
    }
    
    pub fn caesar(n: u8, s: &str) -> String {
        s.chars().map(|c| shift_char(n, c)).collect()
    }
    
    pub fn decrypt(n: u8, s: &str) -> String {
        caesar(26 - n, s)
    }
    
    // ---------------------------------------------------------------------------
    // Approach B: Fold-based — build string with fold
    // ---------------------------------------------------------------------------
    
    pub fn caesar_fold(n: u8, s: &str) -> String {
        s.chars()
            .fold(String::with_capacity(s.len()), |mut acc, c| {
                acc.push(shift_char(n, c));
                acc
            })
    }
    
    // ---------------------------------------------------------------------------
    // Approach C: In-place on bytes (for ASCII-only)
    // ---------------------------------------------------------------------------
    
    pub fn caesar_bytes(n: u8, s: &str) -> String {
        let mut bytes = s.as_bytes().to_vec();
        for b in bytes.iter_mut() {
            *b = match *b {
                b'a'..=b'z' => b'a' + (*b - b'a' + n) % 26,
                b'A'..=b'Z' => b'A' + (*b - b'A' + n) % 26,
                _ => *b,
            };
        }
        String::from_utf8(bytes).unwrap()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_rot13() {
            assert_eq!(caesar(13, "Hello World"), "Uryyb Jbeyq");
        }
    
        #[test]
        fn test_roundtrip() {
            let msg = "Hello World";
            assert_eq!(decrypt(13, &caesar(13, msg)), msg);
        }
    
        #[test]
        fn test_rot0() {
            assert_eq!(caesar(0, "abc"), "abc");
        }
    
        #[test]
        fn test_rot26() {
            assert_eq!(caesar(26, "abc"), "abc");
        }
    
        #[test]
        fn test_non_alpha() {
            assert_eq!(caesar(5, "Hello, World! 123"), "Mjqqt, Btwqi! 123");
        }
    
        #[test]
        fn test_fold_matches() {
            assert_eq!(caesar(7, "Test"), caesar_fold(7, "Test"));
        }
    
        #[test]
        fn test_bytes_matches() {
            assert_eq!(caesar(7, "Test"), caesar_bytes(7, "Test"));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_rot13() {
            assert_eq!(caesar(13, "Hello World"), "Uryyb Jbeyq");
        }
    
        #[test]
        fn test_roundtrip() {
            let msg = "Hello World";
            assert_eq!(decrypt(13, &caesar(13, msg)), msg);
        }
    
        #[test]
        fn test_rot0() {
            assert_eq!(caesar(0, "abc"), "abc");
        }
    
        #[test]
        fn test_rot26() {
            assert_eq!(caesar(26, "abc"), "abc");
        }
    
        #[test]
        fn test_non_alpha() {
            assert_eq!(caesar(5, "Hello, World! 123"), "Mjqqt, Btwqi! 123");
        }
    
        #[test]
        fn test_fold_matches() {
            assert_eq!(caesar(7, "Test"), caesar_fold(7, "Test"));
        }
    
        #[test]
        fn test_bytes_matches() {
            assert_eq!(caesar(7, "Test"), caesar_bytes(7, "Test"));
        }
    }

    Deep Comparison

    Comparison: Caesar Cipher — OCaml vs Rust

    Core Insight

    OCaml's String.map is a single function call that transforms every character. Rust requires the iterator chain chars().map(f).collect() — more explicit but equally expressive. The key difference is that Rust distinguishes char (Unicode scalar) from u8 (byte), giving you a choice between correctness and performance for ASCII workloads.

    OCaml

    let shift_char n c =
      if c >= 'a' && c <= 'z' then
        Char.chr ((Char.code c - Char.code 'a' + n) mod 26 + Char.code 'a')
      else c
    
    let caesar n s = String.map (shift_char n) s
    

    Rust — Idiomatic

    pub fn caesar(n: u8, s: &str) -> String {
        s.chars().map(|c| shift_char(n, c)).collect()
    }
    

    Comparison Table

    AspectOCamlRust
    String mapString.map f ss.chars().map(f).collect()
    Char to intChar.code cc as u8
    Int to charChar.chr nn as char
    Range checkc >= 'a' && c <= 'z''a'..='z' pattern
    Byte accesss.[i]s.as_bytes()[i]
    MutabilityStrings immutableCan mutate byte vec

    Learner Notes

  • Range patterns: Rust's 'a'..='z' in match arms is cleaner than OCaml's boolean comparisons
  • Byte optimization: For ASCII-only ciphers, as_bytes().to_vec() avoids UTF-8 overhead
  • Type safety: Rust's u8 arithmetic catches overflow at compile time with checked ops
  • • **No String.map**: Rust deliberately omits it because strings are UTF-8; the iterator chain makes the per-char cost visible
  • Exercises

  • Add a rot13(s: &str) -> String convenience function that calls caesar(13, s).
  • Implement a Vigenère cipher: instead of a fixed shift, accept a key: &str and cycle through its characters as shifts using .cycle().
  • Write shift_char_wrapping that also handles digits (0–9 shifted mod 10) in addition to letters.
  • Benchmark the three implementations for a 1MB string and compare throughput.
  • In OCaml, implement the Vigenère cipher using Seq.zip to pair message characters with cycled key characters.
  • Open Source Repos