Fibonacci Variants
Tutorial Video
Text description (accessibility)
This video demonstrates the "Fibonacci Variants" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Recursion, Iteration, Accumulators, Fold. Compute the nth Fibonacci number using three different strategies: direct recursion, tail-recursive accumulator, and a fold over a range — then add an idiomatic iterative Rust version for comparison. Key difference from OCaml: 1. **Tail
Tutorial
The Problem
Compute the nth Fibonacci number using three different strategies: direct recursion, tail-recursive accumulator, and a fold over a range — then add an idiomatic iterative Rust version for comparison.
🎯 Learning Outcomes
List.fold_left over a dummy list translates to (0..n).fold over a rangeu64 cleanly handles the base cases without if/else🦀 The Rust Way
Rust mirrors each OCaml variant directly. fib_naive is a straightforward match; fib_tail uses a nested fn go with the same accumulator pattern; fib_fold uses (0..n).fold. A fourth fib_iter variant uses an explicit for loop, which is the most idiomatic Rust style and avoids any stack concerns.
Code Example
pub fn fib_iter(n: u64) -> u64 {
let mut a = 0u64;
let mut b = 1u64;
for _ in 0..n {
let next = a + b;
a = b;
b = next;
}
a
}Key Differences
fib_tail could theoretically overflow for very large n. The iterative fib_iter is the safe Rust default.List.init n Fun.id dummy list; Rust folds over a 0..n range — no allocation needed.match arm n => … binds the same variable name, which is clean and identical in structure to OCaml's function clauses.fib_iter uses two mut locals, which would be unidiomatic in OCaml; in Rust it is perfectly natural and zero-cost.OCaml Approach
OCaml uses three styles: naked double recursion (fib_naive), an inner go function that threads accumulator values and is tail-call optimised by the compiler (fib_tail), and a fold over a dummy integer list created with List.init (fib_fold). The tail-recursive version is idiomatic OCaml for linear recursion.
Full Source
#![allow(clippy::all)]
// Solution 1: Direct recursion — mirrors OCaml `fib_naive`
// Exponential time; useful only for illustration and small n.
pub fn fib_naive(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
n => fib_naive(n - 1) + fib_naive(n - 2),
}
}
// Solution 2: Tail-recursive with accumulator — mirrors OCaml `fib_tail`
// Linear time, O(1) stack in optimised builds (Rust does not guarantee TCO,
// but the recursion depth is bounded by n which is fine for typical inputs).
pub fn fib_tail(n: u64) -> u64 {
fn go(a: u64, b: u64, n: u64) -> u64 {
match n {
0 => a,
n => go(b, a + b, n - 1),
}
}
go(0, 1, n)
}
// Solution 3: Fold-based — mirrors OCaml `fib_fold`
// Uses `(0..n).fold` instead of `List.fold_left` on a dummy list.
pub fn fib_fold(n: u64) -> u64 {
let (a, _) = (0..n).fold((0u64, 1u64), |(a, b), _| (b, a + b));
a
}
// Solution 4: Idiomatic Rust iterator — explicit state machine
// Most natural Rust style: `Iterator` with internal mutable state.
pub fn fib_iter(n: u64) -> u64 {
let mut a = 0u64;
let mut b = 1u64;
for _ in 0..n {
let next = a + b;
a = b;
b = next;
}
a
}
#[cfg(test)]
mod tests {
use super::*;
const KNOWN: &[(u64, u64)] = &[(0, 0), (1, 1), (2, 1), (3, 2), (5, 5), (10, 55), (20, 6765)];
#[test]
fn test_naive_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_naive(n), expected, "fib_naive({n})");
}
}
#[test]
fn test_tail_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_tail(n), expected, "fib_tail({n})");
}
}
#[test]
fn test_fold_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_fold(n), expected, "fib_fold({n})");
}
}
#[test]
fn test_iter_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_iter(n), expected, "fib_iter({n})");
}
}
#[test]
fn test_all_implementations_agree() {
for n in 0..=20u64 {
let expected = fib_naive(n);
assert_eq!(fib_tail(n), expected, "tail disagrees at {n}");
assert_eq!(fib_fold(n), expected, "fold disagrees at {n}");
assert_eq!(fib_iter(n), expected, "iter disagrees at {n}");
}
}
#[test]
fn test_base_cases() {
assert_eq!(fib_naive(0), 0);
assert_eq!(fib_naive(1), 1);
assert_eq!(fib_tail(0), 0);
assert_eq!(fib_tail(1), 1);
assert_eq!(fib_fold(0), 0);
assert_eq!(fib_fold(1), 1);
assert_eq!(fib_iter(0), 0);
assert_eq!(fib_iter(1), 1);
}
#[test]
fn test_larger_value() {
// fib(30) = 832040
assert_eq!(fib_tail(30), 832_040);
assert_eq!(fib_fold(30), 832_040);
assert_eq!(fib_iter(30), 832_040);
}
}#[cfg(test)]
mod tests {
use super::*;
const KNOWN: &[(u64, u64)] = &[(0, 0), (1, 1), (2, 1), (3, 2), (5, 5), (10, 55), (20, 6765)];
#[test]
fn test_naive_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_naive(n), expected, "fib_naive({n})");
}
}
#[test]
fn test_tail_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_tail(n), expected, "fib_tail({n})");
}
}
#[test]
fn test_fold_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_fold(n), expected, "fib_fold({n})");
}
}
#[test]
fn test_iter_known_values() {
for &(n, expected) in KNOWN {
assert_eq!(fib_iter(n), expected, "fib_iter({n})");
}
}
#[test]
fn test_all_implementations_agree() {
for n in 0..=20u64 {
let expected = fib_naive(n);
assert_eq!(fib_tail(n), expected, "tail disagrees at {n}");
assert_eq!(fib_fold(n), expected, "fold disagrees at {n}");
assert_eq!(fib_iter(n), expected, "iter disagrees at {n}");
}
}
#[test]
fn test_base_cases() {
assert_eq!(fib_naive(0), 0);
assert_eq!(fib_naive(1), 1);
assert_eq!(fib_tail(0), 0);
assert_eq!(fib_tail(1), 1);
assert_eq!(fib_fold(0), 0);
assert_eq!(fib_fold(1), 1);
assert_eq!(fib_iter(0), 0);
assert_eq!(fib_iter(1), 1);
}
#[test]
fn test_larger_value() {
// fib(30) = 832040
assert_eq!(fib_tail(30), 832_040);
assert_eq!(fib_fold(30), 832_040);
assert_eq!(fib_iter(30), 832_040);
}
}
Deep Comparison
OCaml vs Rust: Fibonacci Variants
Side-by-Side Code
OCaml
(* Direct recursion *)
let rec fib_naive = function
| 0 -> 0 | 1 -> 1
| n -> fib_naive (n-1) + fib_naive (n-2)
(* Tail-recursive with accumulator *)
let fib_tail n =
let rec go a b = function
| 0 -> a
| n -> go b (a + b) (n - 1)
in go 0 1 n
(* Fold-based *)
let fib_fold n =
let a, _ = List.init n Fun.id
|> List.fold_left (fun (a, b) _ -> (b, a + b)) (0, 1)
in a
Rust (idiomatic — iterative loop)
pub fn fib_iter(n: u64) -> u64 {
let mut a = 0u64;
let mut b = 1u64;
for _ in 0..n {
let next = a + b;
a = b;
b = next;
}
a
}
Rust (functional/recursive — mirrors OCaml fib_tail)
pub fn fib_tail(n: u64) -> u64 {
fn go(a: u64, b: u64, n: u64) -> u64 {
match n {
0 => a,
n => go(b, a + b, n - 1),
}
}
go(0, 1, n)
}
Rust (fold-based — mirrors OCaml fib_fold)
pub fn fib_fold(n: u64) -> u64 {
let (a, _) = (0..n).fold((0u64, 1u64), |(a, b), _| (b, a + b));
a
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Naive recursion | val fib_naive : int -> int | fn fib_naive(n: u64) -> u64 |
| Tail-recursive | val fib_tail : int -> int | fn fib_tail(n: u64) -> u64 |
| Fold-based | val fib_fold : int -> int | fn fib_fold(n: u64) -> u64 |
| Accumulator state | (a, b) threaded through go | (a, b) tuple in .fold closure |
| Fold input | List.init n Fun.id (dummy list) | 0..n (range, zero allocation) |
Key Insights
go b (a+b) (n-1) into a jump. Rust provides no such guarantee, so the recursive fib_tail accumulates stack frames for large n. For safety, fib_iter is preferred in production Rust code.fib_fold creates a dummy list with List.init n Fun.id just to have something to fold over — a common OCaml idiom. Rust avoids allocation entirely by folding over a 0..n range iterator; the semantics are identical but the performance is better.let rec go ... in go 0 1 n and Rust's fn go(...) { ... }; go(0, 1, n) use a local helper to hide the accumulator from the public API. The idiom is structurally identical across both languages.int). Rust requires an explicit type: u64 is chosen here to handle values up to fib(93) without overflow. For larger inputs a u128 or BigInt crate would be needed.function | 0 -> ... | 1 -> ... | n -> ... maps directly to Rust match n { 0 => ..., 1 => ..., n => ... }. Both bind n in the catch-all arm; the structural parallel is exact.When to Use Each Style
**Use idiomatic Rust (fib_iter) when:** you want safe, stack-overflow-free code for any input size with zero overhead.
**Use fold-based (fib_fold) when:** you are composing with other iterator adaptors or want a purely expression-oriented style without mutable variables.
**Use recursive (fib_tail) when:** demonstrating the OCaml accumulator pattern to learners, or when the problem structure is naturally recursive and input is bounded (e.g., ≤ 10 000).
**Avoid fib_naive in production:** its O(2ⁿ) time complexity makes it unusable for any n > ~35.
Exercises
generalized_fibonacci that takes an initial pair (a, b) and computes the n-th term, then verify that golden_ratio emerges from fib(n+1) / fib(n) as n grows.fib(50) and explain the asymptotic complexity of each.