091 — Caesar Cipher
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
(c as u8 - b'a' + n) % 26 for letter rotations.chars().map(shift_char).collect::<String>() for idiomatic transformationfold with String::with_capacity to avoid repeated reallocation.chars().map().collect() to OCaml's String.map f sdecrypt(n) is just caesar(26 - n) — a clean mathematical inverseCode Example
pub fn caesar(n: u8, s: &str) -> String {
s.chars().map(|c| shift_char(n, c)).collect()
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Map over string | .chars().map(f).collect() | String.map f s |
| Char to int | c as u8 | Char.code c |
| Int to char | x as char | Char.chr x |
| Byte ranges | b'a'..=b'z' | c >= 'a' && c <= 'z' |
| In-place | Vec<u8> + from_utf8 | Not idiomatic |
| Decrypt | caesar(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"));
}
}#[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
| Aspect | OCaml | Rust |
|---|---|---|
| String map | String.map f s | s.chars().map(f).collect() |
| Char to int | Char.code c | c as u8 |
| Int to char | Char.chr n | n as char |
| Range check | c >= 'a' && c <= 'z' | 'a'..='z' pattern |
| Byte access | s.[i] | s.as_bytes()[i] |
| Mutability | Strings immutable | Can mutate byte vec |
Learner Notes
'a'..='z' in match arms is cleaner than OCaml's boolean comparisonsas_bytes().to_vec() avoids UTF-8 overheadu8 arithmetic catches overflow at compile time with checked opsString.map**: Rust deliberately omits it because strings are UTF-8; the iterator chain makes the per-char cost visibleExercises
rot13(s: &str) -> String convenience function that calls caesar(13, s).key: &str and cycle through its characters as shifts using .cycle().shift_char_wrapping that also handles digits (0–9 shifted mod 10) in addition to letters.Seq.zip to pair message characters with cycled key characters.