Lazy Fibonacci
Tutorial Video
Text description (accessibility)
This video demonstrates the "Lazy Fibonacci" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Lazy / Infinite Sequences. Generate an infinite stream of Fibonacci numbers and take the first `n` of them, without computing (or storing) the whole sequence up front. Key difference from OCaml: 1. **Lazy abstraction:** OCaml uses a bespoke `stream` type; Rust uses the
Tutorial
The Problem
Generate an infinite stream of Fibonacci numbers and take the first n of them,
without computing (or storing) the whole sequence up front.
🎯 Learning Outcomes
Iterator trait models lazy, potentially-infinite sequencesstream type using Box<dyn Fn()>Box) in Rustmove closures)🦀 The Rust Way
Idiomatic Rust uses the Iterator trait. A FibIter struct holds only two
u64 values and advances them in next(). The standard library's .take(n)
and .collect() adapters replace OCaml's take function. No heap allocation is
needed and the compiler can often unroll or vectorise the loop.
Thunk-based Rust mirrors OCaml directly: Stream<T> holds a head and a
Box<dyn Fn() -> Stream<T>> tail. Box is required because Stream contains
itself β without indirection the type would have infinite size. A move closure
captures a and b by value, keeping each tail self-contained.
Code Example
pub struct FibIter { a: u64, b: u64 }
impl Iterator for FibIter {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.a;
let next_b = self.a + self.b;
self.a = self.b;
self.b = next_b;
Some(value)
}
}
// Usage: FibIter { a: 0, b: 1 }.take(10).collect::<Vec<_>>()Key Differences
stream type; Rust uses the built-in Iterator trait that the entire standard library understands.
requires an explicit Box for any recursive type to bound its stack size.
move closures take ownership, making each thunk independent and 'static.
you only force finite prefixes; neither will diverge on take n.
OCaml Approach
OCaml defines a coinductive 'a stream = Cons of 'a * (unit -> 'a stream).
The tail is a thunk β a suspended computation fun () -> ... β evaluated only
when needed. fibs a b = Cons (a, fun () -> fibs b (a+b)) creates an infinite
structure; take n forces exactly n steps, never evaluating the rest.
Full Source
#![allow(clippy::all)]
// Example 255: Lazy Fibonacci β Infinite stream using closures/iterators
//
// OCaml uses a recursive `stream` type with thunks (`unit -> 'a stream`) to
// model infinite lazy sequences. Rust offers two natural analogues:
// 1. A custom `Stream` struct that boxes the thunk (mirrors OCaml directly)
// 2. `std::iter::Iterator` β the idiomatic Rust lazy-sequence abstraction
// ---------------------------------------------------------------------------
// Solution 1: Idiomatic Rust β Iterator
//
// Rust's Iterator trait is the canonical lazy sequence.
// `FibIter` holds only the two most-recent values; no heap allocation needed.
// ---------------------------------------------------------------------------
/// Infinite Fibonacci iterator starting from (a, b).
pub struct FibIter {
a: u64,
b: u64,
}
impl FibIter {
/// Create a new Fibonacci iterator.
/// `fibs()` starts at 0, 1, 1, 2, 3, 5, β¦
pub fn new(a: u64, b: u64) -> Self {
Self { a, b }
}
}
impl Iterator for FibIter {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.a;
// Advance: (a, b) β (b, a + b)
let next_b = self.a + self.b;
self.a = self.b;
self.b = next_b;
Some(value) // infinite β always Some
}
}
/// Collect the first `n` Fibonacci numbers.
pub fn fibs_take(n: usize) -> Vec<u64> {
FibIter::new(0, 1).take(n).collect()
}
// ---------------------------------------------------------------------------
// Solution 2: Functional / thunk-based β mirrors the OCaml stream type
//
// OCaml: type 'a stream = Cons of 'a * (unit -> 'a stream)
//
// We encode this with a `Box<dyn Fn() -> Stream<T>>` thunk.
// The `Box` is necessary because a recursive type must have known size.
// ---------------------------------------------------------------------------
/// A singly-linked lazy stream; each tail is a heap-allocated thunk.
pub struct Stream<T> {
pub head: T,
// `Box<dyn Fn()>` gives us a heap-allocated, callable thunk β
// exactly like OCaml's `unit -> 'a stream`.
tail: Box<dyn Fn() -> Stream<T>>,
}
impl<T: Copy> Stream<T> {
/// Collect the first `n` elements into a `Vec`.
pub fn take(&self, n: usize) -> Vec<T> {
let mut result = Vec::with_capacity(n);
if n == 0 {
return result;
}
result.push(self.head);
let mut next = (self.tail)();
for _ in 1..n {
result.push(next.head);
next = (next.tail)();
}
result
}
}
/// Build an infinite Fibonacci stream starting from `(a, b)`.
///
/// Mirrors the OCaml: `let rec fibs a b = Cons (a, fun () -> fibs b (a + b))`
pub fn fibs_stream(a: u64, b: u64) -> Stream<u64> {
Stream {
head: a,
tail: Box::new(move || fibs_stream(b, a + b)),
}
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// -- Iterator-based tests ------------------------------------------------
#[test]
fn test_iter_empty() {
let result: Vec<u64> = FibIter::new(0, 1).take(0).collect();
assert_eq!(result, vec![]);
}
#[test]
fn test_iter_single() {
let result: Vec<u64> = FibIter::new(0, 1).take(1).collect();
assert_eq!(result, vec![0]);
}
#[test]
fn test_iter_ten() {
assert_eq!(fibs_take(10), vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
}
#[test]
fn test_iter_large() {
// fib(0..19): 0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181
let v = fibs_take(20);
assert_eq!(v[19], 4181);
}
#[test]
fn test_iter_non_standard_start() {
// Starting from (1, 1) yields Lucas-adjacent sequence
let result: Vec<u64> = FibIter::new(1, 1).take(5).collect();
assert_eq!(result, vec![1, 1, 2, 3, 5]);
}
// -- Stream (thunk) tests ------------------------------------------------
#[test]
fn test_stream_empty() {
let s = fibs_stream(0, 1);
assert_eq!(s.take(0), vec![]);
}
#[test]
fn test_stream_single() {
let s = fibs_stream(0, 1);
assert_eq!(s.take(1), vec![0]);
}
#[test]
fn test_stream_ten() {
let s = fibs_stream(0, 1);
assert_eq!(s.take(10), vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
}
#[test]
fn test_stream_matches_iter() {
let iter_result = fibs_take(15);
let stream_result = fibs_stream(0, 1).take(15);
assert_eq!(iter_result, stream_result);
}
}#[cfg(test)]
mod tests {
use super::*;
// -- Iterator-based tests ------------------------------------------------
#[test]
fn test_iter_empty() {
let result: Vec<u64> = FibIter::new(0, 1).take(0).collect();
assert_eq!(result, vec![]);
}
#[test]
fn test_iter_single() {
let result: Vec<u64> = FibIter::new(0, 1).take(1).collect();
assert_eq!(result, vec![0]);
}
#[test]
fn test_iter_ten() {
assert_eq!(fibs_take(10), vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
}
#[test]
fn test_iter_large() {
// fib(0..19): 0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181
let v = fibs_take(20);
assert_eq!(v[19], 4181);
}
#[test]
fn test_iter_non_standard_start() {
// Starting from (1, 1) yields Lucas-adjacent sequence
let result: Vec<u64> = FibIter::new(1, 1).take(5).collect();
assert_eq!(result, vec![1, 1, 2, 3, 5]);
}
// -- Stream (thunk) tests ------------------------------------------------
#[test]
fn test_stream_empty() {
let s = fibs_stream(0, 1);
assert_eq!(s.take(0), vec![]);
}
#[test]
fn test_stream_single() {
let s = fibs_stream(0, 1);
assert_eq!(s.take(1), vec![0]);
}
#[test]
fn test_stream_ten() {
let s = fibs_stream(0, 1);
assert_eq!(s.take(10), vec![0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
}
#[test]
fn test_stream_matches_iter() {
let iter_result = fibs_take(15);
let stream_result = fibs_stream(0, 1).take(15);
assert_eq!(iter_result, stream_result);
}
}
Deep Comparison
OCaml vs Rust: Lazy Fibonacci Stream
Side-by-Side Code
OCaml
type 'a stream = Cons of 'a * (unit -> 'a stream)
let rec fibs a b = Cons (a, fun () -> fibs b (a + b))
let rec take n (Cons (x, rest)) =
if n = 0 then []
else x :: take (n - 1) (rest ())
let () =
let fib_stream = fibs 0 1 in
List.iter (Printf.printf "%d ") (take 10 fib_stream)
Rust (idiomatic β Iterator)
pub struct FibIter { a: u64, b: u64 }
impl Iterator for FibIter {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let value = self.a;
let next_b = self.a + self.b;
self.a = self.b;
self.b = next_b;
Some(value)
}
}
// Usage: FibIter { a: 0, b: 1 }.take(10).collect::<Vec<_>>()
Rust (functional β thunk-based, mirrors OCaml)
pub struct Stream<T> {
pub head: T,
tail: Box<dyn Fn() -> Stream<T>>, // the thunk
}
pub fn fibs_stream(a: u64, b: u64) -> Stream<u64> {
Stream {
head: a,
tail: Box::new(move || fibs_stream(b, a + b)),
}
}
Type Signatures
| Concept | OCaml | Rust (Iterator) | Rust (Stream) |
|---|---|---|---|
| Stream type | 'a stream | impl Iterator<Item=u64> | Stream<u64> |
| Infinite generator | val fibs : int -> int -> int stream | FibIter::new(u64, u64) -> FibIter | fn fibs_stream(u64, u64) -> Stream<u64> |
| Take prefix | val take : int -> 'a stream -> 'a list | .take(n).collect::<Vec<_>>() | stream.take(n) -> Vec<u64> |
| Thunk type | unit -> 'a stream | N/A (implicit in next) | Box<dyn Fn() -> Stream<T>> |
Key Insights
Iterator is Rust's stream abstraction.** OCaml's stream type is a library-level concept; Rust bakes lazy sequences into the language via the
Iterator trait. The entire std ecosystem (.map, .filter, .take,
.zip, ...) composes freely with any Iterator, including infinite ones.
Box.** OCaml's GC tracks object sizes at runtime, so type 'a stream = Cons of 'a * (unit -> 'a stream) compiles fine. In
Rust every type must have a statically-known size. Stream<T> contains a
Box<dyn Fn()...> (pointer β fixed 16 bytes) rather than Stream<T> itself
(infinite size), making the type representable.
move closures replace GC-managed capture.** OCaml closures automatically capture variables from the enclosing scope via the GC. Rust's move keyword
transfers ownership of a and b into the closure, making it 'static and
independent of any stack frame β a necessary trade-off for memory safety
without GC.
FibIter approach stores only two u64 values on the stack. OCaml's stream allocates a Cons cell on
the heap for every element produced. Rust lets you choose the trade-off:
use the Iterator for zero allocation, or the Stream type to mirror OCaml's
structure exactly.
FibIter implements Iterator, you can write FibIter::new(0,1).filter(|x| x % 2 == 0).take(5).sum::<u64>() with no
extra code. The thunk-based Stream would need manual implementations of
each combinator.
When to Use Each Style
Use idiomatic Rust (Iterator) when: you want zero-allocation lazy sequences
that compose naturally with the rest of std, which is almost always.
Use thunk-based Rust (Stream) when: you are teaching the OCamlβRust
translation, need an explicit coinductive structure (e.g. for tree streams), or
want to experiment with custom stream combinators that diverge from Iterator.
Exercises
(a, b) and a step function (T, T) -> T.