ExamplesBy LevelBy TopicLearning Paths
255 Intermediate

Lazy Fibonacci

Lazy / Infinite Sequences

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

  • β€’ How Rust's Iterator trait models lazy, potentially-infinite sequences
  • β€’ How to encode OCaml's thunk-based stream type using Box<dyn Fn()>
  • β€’ Why recursive types require heap indirection (Box) in Rust
  • β€’ How ownership interacts with closures that capture state (move 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

  • Lazy abstraction: OCaml uses a bespoke stream type; Rust uses the
  • built-in Iterator trait that the entire standard library understands.

  • Heap indirection: OCaml's GC handles recursive types transparently; Rust
  • requires an explicit Box for any recursive type to bound its stack size.

  • Closure capture: OCaml closures capture by reference (GC-managed); Rust
  • move closures take ownership, making each thunk independent and 'static.

  • Infinite safety: Both languages are safe with infinite streams so long as
  • 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust (Iterator)Rust (Stream)
    Stream type'a streamimpl Iterator<Item=u64>Stream<u64>
    Infinite generatorval fibs : int -> int -> int streamFibIter::new(u64, u64) -> FibIterfn fibs_stream(u64, u64) -> Stream<u64>
    Take prefixval take : int -> 'a stream -> 'a list.take(n).collect::<Vec<_>>()stream.take(n) -> Vec<u64>
    Thunk typeunit -> 'a streamN/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.

  • **Recursive types require 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.

  • No-allocation iteration is possible. The 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.

  • Composability. Because 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

  • Extend the lazy Fibonacci iterator to produce a generic linear recurrence: parameterize it with an initial pair (a, b) and a step function (T, T) -> T.
  • Use the lazy iterator to compute the first Fibonacci number greater than one million without materializing earlier values.
  • Implement a lazy Sieve of Eratosthenes using Rust iterators, generating prime numbers on demand, and use it to find all primes less than 10,000.
  • Open Source Repos