938-unfold — Unfold
Tutorial
The Problem
Fold (catamorphism) consumes a recursive structure into a value. Unfold (anamorphism) generates a recursive structure from a seed value. They are dual operations. Unfold takes a seed S and a function f: S -> Option<(T, S)>: if f(s) = None, the sequence ends; if f(s) = Some(value, next_state), emit value and continue with next_state. This generates sequences: ranges, Collatz sequences, Fibonacci, countdowns. Haskell has unfoldr; OCaml has Seq.unfold. Rust's std::iter::from_fn and custom iterators serve the same role. Unfold is the generative dual of fold.
🎯 Learning Outcomes
unfold<T, S, F> functionOption<(T, S)> as the protocol: None terminates, Some((value, next)) continuesSeq.unfold and Haskell's Data.List.unfoldrCode Example
#![allow(clippy::all)]
/// # Unfold — Generating Sequences from Seeds
///
/// Unfold is the dual of fold: fold consumes a list into a value,
/// unfold produces a list from a seed value.
/// Generic unfold: applies f to seed repeatedly until f returns None.
/// Returns a Vec of produced values.
pub fn unfold<T, S, F>(seed: S, f: F) -> Vec<T>
where
F: Fn(S) -> Option<(T, S)>,
S: Clone,
{
let mut result = Vec::new();
let mut state = seed;
while let Some((value, next)) = f(state.clone()) {
result.push(value);
state = next;
}
result
}
/// Range using unfold
pub fn range(a: i64, b: i64) -> Vec<i64> {
unfold(a, |i| if i > b { None } else { Some((i, i + 1)) })
}
/// Countdown using unfold
pub fn countdown(n: i64) -> Vec<i64> {
unfold(n, |i| if i < 0 { None } else { Some((i, i - 1)) })
}
/// Collatz sequence using unfold
pub fn collatz(n: u64) -> Vec<u64> {
unfold(n, |x| {
if x == 0 {
None
} else if x == 1 {
Some((1, 0))
} else if x % 2 == 0 {
Some((x, x / 2))
} else {
Some((x, 3 * x + 1))
}
})
}
/// Iterator-based unfold using `std::iter::successors`
pub fn fibs_iter() -> impl Iterator<Item = u64> {
std::iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b))).map(|(a, _)| a)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_range() {
assert_eq!(range(1, 5), vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_range_empty() {
assert_eq!(range(5, 3), vec![]);
}
#[test]
fn test_countdown() {
assert_eq!(countdown(5), vec![5, 4, 3, 2, 1, 0]);
}
#[test]
fn test_collatz() {
assert_eq!(collatz(6), vec![6, 3, 10, 5, 16, 8, 4, 2, 1]);
}
#[test]
fn test_collatz_one() {
assert_eq!(collatz(1), vec![1]);
}
#[test]
fn test_fibs_iter() {
let first8: Vec<u64> = fibs_iter().take(8).collect();
assert_eq!(first8, vec![0, 1, 1, 2, 3, 5, 8, 13]);
}
}Key Differences
unfold is eager (returns Vec); OCaml's Seq.unfold is lazy; Rust's from_fn closure enables lazy unfold.S: Clone to apply f and keep state; the lazy iterator version can take ownership.Option<(value, next_state)> — same semantic protocol, just Option vs None/Some.Seq.unfold since 4.11; Rust uses std::iter::from_fn or custom iterators — no direct unfold function in std.OCaml Approach
Seq.unfold: ('a -> ('b * 'a) option) -> 'a -> 'b Seq.t is the direct equivalent (since 4.11). let range a b = Seq.unfold (fun i -> if i > b then None else Some(i, i+1)) a. Fibonacci: Seq.unfold (fun (a,b) -> Some(a, (b, a+b))) (0, 1). OCaml's Seq.unfold is lazy — elements are generated on demand. Converting to list: Seq.take n seq |> List.of_seq. Haskell's unfoldr uses (a -> Maybe (b, a)) — same protocol, different Maybe syntax.
Full Source
#![allow(clippy::all)]
/// # Unfold — Generating Sequences from Seeds
///
/// Unfold is the dual of fold: fold consumes a list into a value,
/// unfold produces a list from a seed value.
/// Generic unfold: applies f to seed repeatedly until f returns None.
/// Returns a Vec of produced values.
pub fn unfold<T, S, F>(seed: S, f: F) -> Vec<T>
where
F: Fn(S) -> Option<(T, S)>,
S: Clone,
{
let mut result = Vec::new();
let mut state = seed;
while let Some((value, next)) = f(state.clone()) {
result.push(value);
state = next;
}
result
}
/// Range using unfold
pub fn range(a: i64, b: i64) -> Vec<i64> {
unfold(a, |i| if i > b { None } else { Some((i, i + 1)) })
}
/// Countdown using unfold
pub fn countdown(n: i64) -> Vec<i64> {
unfold(n, |i| if i < 0 { None } else { Some((i, i - 1)) })
}
/// Collatz sequence using unfold
pub fn collatz(n: u64) -> Vec<u64> {
unfold(n, |x| {
if x == 0 {
None
} else if x == 1 {
Some((1, 0))
} else if x % 2 == 0 {
Some((x, x / 2))
} else {
Some((x, 3 * x + 1))
}
})
}
/// Iterator-based unfold using `std::iter::successors`
pub fn fibs_iter() -> impl Iterator<Item = u64> {
std::iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b))).map(|(a, _)| a)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_range() {
assert_eq!(range(1, 5), vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_range_empty() {
assert_eq!(range(5, 3), vec![]);
}
#[test]
fn test_countdown() {
assert_eq!(countdown(5), vec![5, 4, 3, 2, 1, 0]);
}
#[test]
fn test_collatz() {
assert_eq!(collatz(6), vec![6, 3, 10, 5, 16, 8, 4, 2, 1]);
}
#[test]
fn test_collatz_one() {
assert_eq!(collatz(1), vec![1]);
}
#[test]
fn test_fibs_iter() {
let first8: Vec<u64> = fibs_iter().take(8).collect();
assert_eq!(first8, vec![0, 1, 1, 2, 3, 5, 8, 13]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_range() {
assert_eq!(range(1, 5), vec![1, 2, 3, 4, 5]);
}
#[test]
fn test_range_empty() {
assert_eq!(range(5, 3), vec![]);
}
#[test]
fn test_countdown() {
assert_eq!(countdown(5), vec![5, 4, 3, 2, 1, 0]);
}
#[test]
fn test_collatz() {
assert_eq!(collatz(6), vec![6, 3, 10, 5, 16, 8, 4, 2, 1]);
}
#[test]
fn test_collatz_one() {
assert_eq!(collatz(1), vec![1]);
}
#[test]
fn test_fibs_iter() {
let first8: Vec<u64> = fibs_iter().take(8).collect();
assert_eq!(first8, vec![0, 1, 1, 2, 3, 5, 8, 13]);
}
}
Deep Comparison
Unfold — OCaml vs Rust Comparison
Core Insight
Unfold is the categorical dual of fold: where fold consumes a structure into a summary, unfold generates a structure from a seed. OCaml expresses it naturally as a recursive function returning a list. Rust can do the same but also has built-in lazy unfold via std::iter::successors.
OCaml Approach
A simple recursive function: call f seed, if Some(value, next) then cons value onto the recursive result. Clean and elegant, but not tail-recursive — deep sequences can overflow the stack. OCaml's Seq.unfold provides a lazy version.
Rust Approach
Two options: (1) A custom unfold function that collects into Vec — eagerly evaluates the entire sequence. (2) std::iter::successors or std::iter::from_fn for lazy evaluation — generates values on demand. The lazy version is more idiomatic for potentially large or infinite sequences.
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Memory | Cons cells (linked list) | Vec (contiguous) or iterator (lazy) |
| Null safety | Option for termination | Option for termination |
| Errors | Stack overflow on deep unfold | Vec grows dynamically (no overflow) |
| Iteration | Recursive | while let loop or iterator chain |
| Laziness | Seq.unfold (separate module) | successors / from_fn (built-in) |
Things Rust Learners Should Notice
while let Some(...) pattern** — idiomatic for consuming option-producing functionsS: Clone bound** — needed because the state must be passed to f while we check the resultstd::iter::successors** — built-in lazy unfold, returns an iteratorunfold builds a Vec immediately; successors is lazyNone from the function signals end of sequence in both languagesFurther Reading
Exercises
unfold_iter<T, S, F: Fn(S) -> Option<(T, S)>> using std::iter::from_fn that generates values on demand.unfold to generate the sequence of perfect squares below 1000.unfold_tree<T, S, F>(seed: S, f: F) -> Tree<T> where f(s) = None creates a leaf and f(s) = Some(value, left_seed, right_seed) creates a node.