๐Ÿฆ€ Functional Rust

265: Caesar Cipher

Difficulty: 1 Level: Beginner Encrypt and decrypt text by shifting each letter a fixed number of positions in the alphabet, leaving non-letter characters unchanged.

The Problem This Solves

You're learning character manipulation in Rust โ€” how to treat `char` as a number, apply arithmetic, and map transformations over strings. The Caesar cipher is the perfect vehicle: the logic is simple enough to understand immediately, but it forces you to work with character ranges, modular arithmetic, and string collection. Without range patterns and `as u8` casting, you'd write verbose `if/else` chains checking `c >= 'a' && c <= 'z'`. Without `.chars().map().collect()`, you'd iterate over a string building a new one character by character. The Caesar cipher shows you both idioms in under 10 lines. Decryption as `caesar(26 - n, s)` demonstrates a key functional insight: inverse operations can often be expressed as the original operation with a different parameter, avoiding duplicated logic.

The Intuition

Map each letter to its shifted position in the alphabet using modular arithmetic; pass non-letter characters through unchanged.

How It Works in Rust

fn shift_char(n: u8, c: char) -> char {
 match c {
     // Range pattern matches entire alphabet in one arm
     '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,  // non-letters pass through unchanged
 }
}

pub fn caesar(n: u8, s: &str) -> String {
 s.chars()           // iterate over Unicode code points
  .map(|c| shift_char(n, c))  // shift each letter
  .collect()         // reassemble into a String
}

pub fn decrypt(n: u8, s: &str) -> String {
 caesar(26 - (n % 26), s)  // shift by the complement; n % 26 handles n=26
}

pub fn rot13(s: &str) -> String {
 caesar(13, s)  // ROT13 is its own inverse: rot13(rot13(s)) == s
}
The key arithmetic: `(c as u8 - b'a' + n) % 26 + b'a'` 1. Subtract `b'a'` โ†’ 0-based index (0โ€“25) 2. Add shift `n`, take `% 26` โ†’ stays in alphabet 3. Add `b'a'` โ†’ back to ASCII code point 4. Cast to `char`

What This Unlocks

Key Differences

ConceptOCamlRust
Char to int`Char.code c``c as u8`
Int to char`Char.chr n``n as char`
Alphabet check`if c >= 'a' && c <= 'z'``'a'..='z'` range pattern
String mapping`String.map (shift_char n) s``s.chars().map(\c\shift_char(n, c)).collect()`
Decryption`let decrypt n = caesar (26 - n)` (partial app)`fn decrypt(n, s) { caesar(26 - n % 26, s) }`
/// Caesar Cipher โ€” Functional Encryption

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)
}

pub fn rot13(s: &str) -> String {
    caesar(13, s)
}

fn main() {
    let msg = "Hello World";
    let enc = caesar(13, msg);
    println!("Encrypted: {}", enc);
    println!("Decrypted: {}", decrypt(13, &enc));
    println!("ROT13 twice: {}", rot13(&rot13(msg)));
}

/* Output:
   Encrypted: Uryyb Jbeyq
   Decrypted: Hello World
   ROT13 twice: Hello World
*/
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)

let () =
  let msg = "Hello World" in
  let enc = caesar 13 msg in
  assert (enc = "Uryyb Jbeyq");
  assert (decrypt 13 enc = msg);
  (* ROT13 is self-inverse *)
  assert (caesar 13 (caesar 13 "test") = "test");
  print_endline "ok"

๐Ÿ“Š Detailed Comparison

OCaml vs Rust: Caesar Cipher โ€” Functional Encryption

Side-by-Side Code

OCaml

๐Ÿช Show OCaml equivalent
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

ConceptOCamlRust
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

1. Range patterns are elegant: Rust's `'a'..='z'` in match arms is cleaner than OCaml's `if c >= 'a' && c <= 'z'` โ€” pattern matching at its best 2. Byte literals: Rust's `b'a'` is equivalent to OCaml's `Char.code 'a'` โ€” both give the numeric value of the character 3. String ownership: OCaml's `String.map` returns a new string (strings are mutable but `map` creates new); Rust borrows `&str` and returns owned `String` 4. Lazy iteration: Rust's `impl Iterator` approach delays computation; OCaml would need `Seq` for the same laziness 5. Type safety on shift: Rust uses `u8` 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