Caesar Cipher — Functional Encryption
Tutorial Video
Text description (accessibility)
This video demonstrates the "Caesar Cipher — Functional Encryption" functional Rust example. Difficulty level: Fundamental. Key concepts covered: String Processing. Implement a Caesar cipher that shifts each letter by a fixed number of positions in the alphabet. Key difference from OCaml: 1. **Char arithmetic:** OCaml uses `Char.code`/`Char.chr`; Rust casts with `as u8`/`as char`
Tutorial
The Problem
Implement a Caesar cipher that shifts each letter by a fixed number of positions in the alphabet. Non-letter characters pass through unchanged. Provide both encryption and decryption.
🎯 Learning Outcomes
as u8 and as char conversions'a'..='z').chars().map().collect()🦀 The Rust Way
Rust uses as u8 / as char for character arithmetic and range patterns 'a'..='z' in match arms. The iterator chain .chars().map().collect() replaces String.map. Decryption uses the same shift-reversal technique.
Code Example
fn shift_char(n: u8, c: char) -> char {
match c {
'a'..='z' => ((c as u8 - b'a' + n) % 26 + b'a') as char,
'A'..='Z' => ((c as u8 - b'A' + n) % 26 + b'A') 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 % 26), s)
}Key Differences
Char.code/Char.chr; Rust casts with as u8/as charif/else on char comparisons; Rust uses range patterns 'a'..='z'String.map applies a function per char; Rust uses .chars().map().collect()let decrypt n = caesar (26 - n) is more concise; Rust needs a full function definitionOCaml Approach
OCaml uses Char.code and Char.chr for character arithmetic and String.map to apply the shift function to every character. decrypt is elegantly defined as caesar (26 - n) using partial application.
Full Source
#![allow(clippy::all)]
//! Caesar Cipher — Functional Encryption
//!
//! OCaml: `let caesar n s = String.map (shift_char n) s`
//! Rust: `fn caesar(n: u8, s: &str) -> String` using `.chars().map().collect()`
//!
//! A Caesar cipher shifts each letter by a fixed number of positions
//! in the alphabet, wrapping around. Non-letter characters pass through unchanged.
//! Shifts a single character by `n` positions.
//!
//! OCaml: pattern matches on char ranges with `>=` and `<=`.
//! Rust: uses range patterns in match arms.
fn shift_char(n: u8, c: char) -> char {
match c {
'a'..='z' => {
let shifted = (c as u8 - b'a' + n) % 26 + b'a';
shifted as char
}
'A'..='Z' => {
let shifted = (c as u8 - b'A' + n) % 26 + b'A';
shifted as char
}
_ => c,
}
}
/// Encrypts a string using Caesar cipher with shift `n`.
///
/// OCaml: `let caesar n s = String.map (shift_char n) s`
/// Rust uses iterator chain: `.chars().map().collect()`.
pub fn caesar(n: u8, s: &str) -> String {
s.chars().map(|c| shift_char(n, c)).collect()
}
/// Decrypts by shifting in the opposite direction.
///
/// OCaml: `let decrypt n = caesar (26 - n)`
/// Rust: same idea — shift by `26 - n`.
pub fn decrypt(n: u8, s: &str) -> String {
caesar(26 - (n % 26), s)
}
/// ROT13: the classic self-inverse Caesar cipher (shift by 13).
/// Applying it twice returns the original.
pub fn rot13(s: &str) -> String {
caesar(13, s)
}
/// Iterator-based approach: returns a lazy iterator of shifted chars.
pub fn caesar_iter(n: u8, s: &str) -> impl Iterator<Item = char> + '_ {
s.chars().map(move |c| shift_char(n, c))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_encryption() {
assert_eq!(caesar(13, "Hello World"), "Uryyb Jbeyq");
}
#[test]
fn test_decryption_reverses() {
let msg = "Hello World";
let encrypted = caesar(13, msg);
let decrypted = decrypt(13, &encrypted);
assert_eq!(decrypted, msg);
}
#[test]
fn test_rot13_self_inverse() {
let msg = "The Quick Brown Fox";
assert_eq!(rot13(&rot13(msg)), msg);
}
#[test]
fn test_preserves_non_alpha() {
assert_eq!(caesar(5, "Hello, World! 123"), "Mjqqt, Btwqi! 123");
}
#[test]
fn test_zero_shift() {
assert_eq!(caesar(0, "unchanged"), "unchanged");
}
#[test]
fn test_full_rotation() {
assert_eq!(caesar(26, "abc"), "abc");
}
#[test]
fn test_wraparound() {
assert_eq!(caesar(1, "xyz"), "yza");
assert_eq!(caesar(1, "XYZ"), "YZA");
}
#[test]
fn test_empty_string() {
assert_eq!(caesar(5, ""), "");
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_encryption() {
assert_eq!(caesar(13, "Hello World"), "Uryyb Jbeyq");
}
#[test]
fn test_decryption_reverses() {
let msg = "Hello World";
let encrypted = caesar(13, msg);
let decrypted = decrypt(13, &encrypted);
assert_eq!(decrypted, msg);
}
#[test]
fn test_rot13_self_inverse() {
let msg = "The Quick Brown Fox";
assert_eq!(rot13(&rot13(msg)), msg);
}
#[test]
fn test_preserves_non_alpha() {
assert_eq!(caesar(5, "Hello, World! 123"), "Mjqqt, Btwqi! 123");
}
#[test]
fn test_zero_shift() {
assert_eq!(caesar(0, "unchanged"), "unchanged");
}
#[test]
fn test_full_rotation() {
assert_eq!(caesar(26, "abc"), "abc");
}
#[test]
fn test_wraparound() {
assert_eq!(caesar(1, "xyz"), "yza");
assert_eq!(caesar(1, "XYZ"), "YZA");
}
#[test]
fn test_empty_string() {
assert_eq!(caesar(5, ""), "");
}
}
Deep Comparison
OCaml vs Rust: Caesar Cipher — Functional Encryption
Side-by-Side Code
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 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
let decrypt n = caesar (26 - n)
Rust (idiomatic)
fn shift_char(n: u8, c: char) -> char {
match c {
'a'..='z' => ((c as u8 - b'a' + n) % 26 + b'a') as char,
'A'..='Z' => ((c as u8 - b'A' + n) % 26 + b'A') 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 % 26), s)
}
Rust (iterator-based — lazy)
pub fn caesar_iter(n: u8, s: &str) -> impl Iterator<Item = char> + '_ {
s.chars().map(move |c| shift_char(n, c))
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Shift char | val shift_char : int -> char -> char | fn shift_char(n: u8, c: char) -> char |
| Encrypt | val caesar : int -> string -> string | fn caesar(n: u8, s: &str) -> String |
| Decrypt | val decrypt : int -> string -> string | fn decrypt(n: u8, s: &str) -> String |
| Char type | char (8-bit) | char (32-bit Unicode scalar) |
| String type | string (byte sequence) | &str (UTF-8 borrowed) / String (owned) |
Key Insights
'a'..='z' in match arms is cleaner than OCaml's if c >= 'a' && c <= 'z' — pattern matching at its bestb'a' is equivalent to OCaml's Char.code 'a' — both give the numeric value of the characterString.map returns a new string (strings are mutable but map creates new); Rust borrows &str and returns owned Stringimpl Iterator approach delays computation; OCaml would need Seq for the same lazinessu8 for the shift amount, preventing negative shifts at the type level; OCaml uses int which could be negative (handled by mod)When to Use Each Style
**Use eager collection (caesar) when:** you need the full encrypted string as a String for storage or further processing
**Use lazy iteration (caesar_iter) when:** you're chaining with other transformations or writing to a stream character-by-character
Exercises
caesar_crack that performs brute-force decryption: try all 25 shifts and return the one whose output most closely matches English letter frequencies.