๐Ÿฆ€ Functional Rust
๐ŸŽฌ Traits & Generics Shared behaviour, static vs dynamic dispatch, zero-cost polymorphism.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Traits define shared behaviour โ€” like interfaces but with default implementations

โ€ข Generics with trait bounds: fn process(item: T) โ€” monomorphized at compile time

โ€ข Static dispatch (impl Trait) = zero cost; dynamic dispatch (dyn Trait) = runtime flexibility via vtable

โ€ข Blanket implementations apply traits to all types matching a bound

โ€ข Associated types and supertraits enable complex type relationships

843: Generic Memoization with HashMap Cache

Difficulty: 3 Level: Intermediate Cache pure function results in a `HashMap` to turn exponential recursion into polynomial time โ€” and learn the `RefCell` workaround for recursive self-memoization.

The Problem This Solves

Recursive algorithms with overlapping subproblems recompute the same values exponentially. `fib(50)` with naive recursion makes ~10^10 calls; with memoization, exactly 50 unique calls. This is the same transformation as dynamic programming, but expressed top-down as memoized recursion rather than bottom-up table-filling. Both yield the same asymptotic complexity; the choice between them is engineering preference. Memoization appears beyond just Fibonacci: parsing with Earley or CYK algorithms memoizes subparse results, making context-free parsing O(nยณ) instead of exponential. Edit distance, optimal matrix chain multiplication, longest common subsequence โ€” all are exponential without memo, polynomial with it. Understanding memoization lets you convert any recursive solution into an efficient one. The harder challenge in Rust: how do you memoize a recursive function where the memo table must be accessible during the recursive call? The `HashMap` can't be borrowed mutably while a recursive call is happening. The solutions: pass `&mut HashMap` as a parameter (cleanest for Rust), or use `RefCell<HashMap>` for interior mutability when you need the memo table in a closure or struct method.

The Intuition

The time-space tradeoff: pay O(n) memory to reduce O(2^n) computation to O(n). Each unique argument is computed exactly once; subsequent calls return the cached value immediately. The memo table is a `HashMap<K, V>` where K is the argument type and V is the return type. For recursive memoization in Rust: pass `&mut HashMap` down through all recursive calls. This is zero-overhead โ€” no locks, no reference counting โ€” and the borrow checker ensures no aliasing. The `RefCell` pattern is for cases where you can't thread the `&mut` through (e.g., implementing a memoized method on a struct). OCaml's `Hashtbl` has the same interface; OCaml closures can close over mutable state more naturally, making recursive memoization slightly simpler syntactically.

How It Works in Rust

use std::collections::HashMap;

// Pattern 1: Pass &mut cache explicitly โ€” cleanest Rust idiom
fn fib(n: u64, cache: &mut HashMap<u64, u64>) -> u64 {
 if n <= 1 { return n; }
 if let Some(&v) = cache.get(&n) { return v; }  // Cache hit: O(1)
 let v = fib(n - 1, cache) + fib(n - 2, cache);  // Recursive: each n computed once
 cache.insert(n, v);                               // Store result
 v
}

// Pattern 2: RefCell for interior mutability (when &mut threading is impractical)
use std::cell::RefCell;

fn fib_refcell(n: u64) -> u64 {
 thread_local! {
     static CACHE: RefCell<HashMap<u64, u64>> = RefCell::new(HashMap::new());
 }
 if n <= 1 { return n; }
 if let Some(&v) = CACHE.with(|c| c.borrow().get(&n).copied()) {
     return v;  // Borrow immutably to check
 }
 let v = fib_refcell(n - 1) + fib_refcell(n - 2);
 CACHE.with(|c| c.borrow_mut().insert(n, v));  // Borrow mutably to insert
 v
}

// Coin change: minimum coins to make `amount` โ€” O(amount ร— |coins|)
fn coin_change(coins: &[u64], amount: u64) -> Option<u64> {
 let mut cache: HashMap<u64, Option<u64>> = HashMap::new();
 fn rec(coins: &[u64], amount: u64, cache: &mut HashMap<u64, Option<u64>>) -> Option<u64> {
     if amount == 0 { return Some(0); }
     if let Some(&v) = cache.get(&amount) { return v; }
     let result = coins.iter()
         .filter(|&&c| c <= amount)
         .filter_map(|&c| rec(coins, amount - c, cache).map(|n| n + 1))
         .min();
     cache.insert(amount, result);
     result
 }
 rec(coins, amount, &mut cache)
}
`thread_local!` + `RefCell` is the standard pattern when you want a memoized function without passing the cache explicitly โ€” commonly used in competitive programming where you want global memoization of a recursive function.

What This Unlocks

Key Differences

ConceptOCamlRust
Hash table`Hashtbl.create 16``HashMap::new()`
Lookup`Hashtbl.find_opt tbl key``cache.get(&key)` โ†’ `Option<&V>`
Insert`Hashtbl.replace tbl key v``cache.insert(key, v)`
Recursive memoClose over `tbl` ref โ€” naturalPass `&mut cache` or use `RefCell`
Thread-local cacheNot applicable (single-threaded)`thread_local! { static CACHE: RefCell<HashMap<...>> }`
/// Generic Memoisation with HashMap cache.
///
/// Transforms overlapping-subproblem recursion from exponential to polynomial.
/// Fibonacci, coin change, edit distance.

use std::collections::HashMap;

/// Fibonacci with manual HashMap memoisation.
fn fib(n: u64, cache: &mut HashMap<u64, u64>) -> u64 {
    if n <= 1 { return n; }
    if let Some(&v) = cache.get(&n) { return v; }
    let v = fib(n - 1, cache) + fib(n - 2, cache);
    cache.insert(n, v);
    v
}

/// Coin change: minimum number of coins to make `amount`.
fn coin_change(coins: &[u64], amount: u64) -> Option<u64> {
    let mut cache: HashMap<u64, Option<u64>> = HashMap::new();
    coin_change_rec(coins, amount, &mut cache)
}

fn coin_change_rec(coins: &[u64], amount: u64, cache: &mut HashMap<u64, Option<u64>>) -> Option<u64> {
    if amount == 0 { return Some(0); }
    if let Some(&v) = cache.get(&amount) { return v; }

    let result = coins.iter()
        .filter(|&&c| c <= amount)
        .filter_map(|&c| coin_change_rec(coins, amount - c, cache).map(|n| n + 1))
        .min();

    cache.insert(amount, result);
    result
}

/// Edit distance (Levenshtein) with memoisation.
fn edit_distance(s: &[u8], t: &[u8]) -> usize {
    let mut cache: HashMap<(usize, usize), usize> = HashMap::new();
    edit_rec(s, t, 0, 0, &mut cache)
}

fn edit_rec(
    s: &[u8], t: &[u8],
    i: usize, j: usize,
    cache: &mut HashMap<(usize, usize), usize>,
) -> usize {
    if i == s.len() { return t.len() - j; }
    if j == t.len() { return s.len() - i; }
    if let Some(&v) = cache.get(&(i, j)) { return v; }

    let v = if s[i] == t[j] {
        edit_rec(s, t, i + 1, j + 1, cache)
    } else {
        1 + edit_rec(s, t, i + 1, j, cache)     // delete
            .min(edit_rec(s, t, i, j + 1, cache)) // insert
            .min(edit_rec(s, t, i + 1, j + 1, cache)) // replace
    };
    cache.insert((i, j), v);
    v
}

/// Longest Common Subsequence with memoisation.
fn lcs(s: &[u8], t: &[u8]) -> usize {
    let mut cache: HashMap<(usize, usize), usize> = HashMap::new();
    lcs_rec(s, t, 0, 0, &mut cache)
}

fn lcs_rec(s: &[u8], t: &[u8], i: usize, j: usize, cache: &mut HashMap<(usize, usize), usize>) -> usize {
    if i == s.len() || j == t.len() { return 0; }
    if let Some(&v) = cache.get(&(i, j)) { return v; }
    let v = if s[i] == t[j] {
        1 + lcs_rec(s, t, i + 1, j + 1, cache)
    } else {
        lcs_rec(s, t, i + 1, j, cache).max(lcs_rec(s, t, i, j + 1, cache))
    };
    cache.insert((i, j), v);
    v
}

fn main() {
    let mut cache = HashMap::new();
    println!("Fibonacci (memoised):");
    for n in 0..=15u64 {
        print!("fib({n})={} ", fib(n, &mut cache));
    }
    println!();

    println!("\nCoin change:");
    println!("  coins=[1,5,10,25], amount=41: {:?}", coin_change(&[1, 5, 10, 25], 41));
    println!("  coins=[2], amount=3: {:?}", coin_change(&[2], 3));
    println!("  coins=[1,3,4], amount=6: {:?}", coin_change(&[1, 3, 4], 6));

    println!("\nEdit distance:");
    println!("  'kitten' vs 'sitting': {}", edit_distance(b"kitten", b"sitting")); // 3
    println!("  'abc' vs 'abc': {}", edit_distance(b"abc", b"abc"));               // 0
    println!("  '' vs 'abc': {}", edit_distance(b"", b"abc"));                     // 3

    println!("\nLCS:");
    println!("  'ABCBDAB' vs 'BDCAB': {}", lcs(b"ABCBDAB", b"BDCAB")); // 4
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_fib() {
        let mut cache = HashMap::new();
        assert_eq!(fib(0, &mut cache), 0);
        assert_eq!(fib(1, &mut cache), 1);
        assert_eq!(fib(10, &mut cache), 55);
        assert_eq!(fib(20, &mut cache), 6765);
    }

    #[test]
    fn test_coin_change_basic() {
        assert_eq!(coin_change(&[1, 5, 10, 25], 41), Some(4)); // 25+10+5+1
        assert_eq!(coin_change(&[1, 3, 4], 6), Some(2));       // 3+3
        assert_eq!(coin_change(&[2], 3), None);                 // impossible
    }

    #[test]
    fn test_coin_change_zero() {
        assert_eq!(coin_change(&[1, 5], 0), Some(0));
    }

    #[test]
    fn test_edit_distance() {
        assert_eq!(edit_distance(b"kitten", b"sitting"), 3);
        assert_eq!(edit_distance(b"", b""), 0);
        assert_eq!(edit_distance(b"abc", b"abc"), 0);
        assert_eq!(edit_distance(b"", b"abc"), 3);
        assert_eq!(edit_distance(b"abc", b""), 3);
        assert_eq!(edit_distance(b"a", b"b"), 1);
    }

    #[test]
    fn test_lcs() {
        assert_eq!(lcs(b"ABCBDAB", b"BDCAB"), 4);
        assert_eq!(lcs(b"abc", b"abc"), 3);
        assert_eq!(lcs(b"abc", b"def"), 0);
    }
}
(* Generic Memoisation in OCaml *)

(* Generic memoize: wrap any function f with a hash-table cache *)
let memoize (f : ('a, 'b) Hashtbl.t -> 'a -> 'b) : ('a -> 'b) =
  let cache = Hashtbl.create 64 in
  let rec go arg =
    match Hashtbl.find_opt cache arg with
    | Some v -> v
    | None ->
      let v = f cache arg in
      Hashtbl.add cache arg v;
      v
  in
  ignore go;  (* go is the memoised version, but we need to close over it *)
  (* Better pattern: pass cache explicitly *)
  let cache2 = Hashtbl.create 64 in
  fun arg ->
    match Hashtbl.find_opt cache2 arg with
    | Some v -> v
    | None ->
      let v = f cache2 arg in
      Hashtbl.add cache2 arg v;
      v

(* Fibonacci with memoisation โ€” the classic example *)
let fib_memo : int -> int =
  let cache = Hashtbl.create 64 in
  let rec fib n =
    if n <= 1 then n
    else
      match Hashtbl.find_opt cache n with
      | Some v -> v
      | None ->
        let v = fib (n - 1) + fib (n - 2) in
        Hashtbl.add cache n v;
        v
  in
  fib

(* Coin change: minimum coins to make amount *)
let coin_change (coins : int list) (amount : int) : int option =
  let cache = Hashtbl.create 128 in
  let inf = max_int / 2 in
  let rec go amt =
    if amt = 0 then 0
    else if amt < 0 then inf
    else
      match Hashtbl.find_opt cache amt with
      | Some v -> v
      | None ->
        let best = List.fold_left (fun best c ->
          min best (1 + go (amt - c))
        ) inf coins in
        Hashtbl.add cache amt best;
        best
  in
  let result = go amount in
  if result >= inf then None else Some result

(* Edit distance with memoisation *)
let edit_distance (s : string) (t : string) : int =
  let m = String.length s and n = String.length t in
  let cache = Hashtbl.create 256 in
  let rec go i j =
    if i = m then n - j
    else if j = n then m - i
    else
      match Hashtbl.find_opt cache (i, j) with
      | Some v -> v
      | None ->
        let v =
          if s.[i] = t.[j] then go (i+1) (j+1)
          else 1 + min (go (i+1) j) (min (go i (j+1)) (go (i+1) (j+1)))
        in
        Hashtbl.add cache (i, j) v;
        v
  in
  go 0 0

let () =
  for n = 0 to 15 do
    Printf.printf "fib(%d) = %d\n" n (fib_memo n)
  done;
  Printf.printf "\ncoin_change([1,5,10,25], 41) = %s\n"
    (match coin_change [1;5;10;25] 41 with Some v -> string_of_int v | None -> "None");
  Printf.printf "coin_change([2], 3) = %s\n"
    (match coin_change [2] 3 with Some v -> string_of_int v | None -> "None");
  Printf.printf "\nedit_distance('kitten', 'sitting') = %d  (expected 3)\n"
    (edit_distance "kitten" "sitting");
  Printf.printf "edit_distance('', 'abc') = %d\n" (edit_distance "" "abc")