284: FusedIterator for Stable Termination
Tutorial Video
Text description (accessibility)
This video demonstrates the "284: FusedIterator for Stable Termination" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The `Iterator` specification in Rust says that after `next()` returns `None`, subsequent behavior is undefined — some iterators might return `Some` again if called further (a "restart"). Key difference from OCaml: 1. **Structural vs behavioral**: OCaml's `Seq.Nil` terminates the sequence structurally (you can't call it again); Rust requires an explicit contract via `FusedIterator`.
Tutorial
The Problem
The Iterator specification in Rust says that after next() returns None, subsequent behavior is undefined — some iterators might return Some again if called further (a "restart"). This makes it unsafe to call next() after termination without an explicit check. The FusedIterator marker trait solves this by promising that once next() returns None, all future calls also return None — enabling safe and optimized composition in adapters that call next() multiple times.
🎯 Learning Outcomes
FusedIterator as a marker trait guaranteeing "once None, always None"FusedIterator correctly (only when the implementation actually fuses)filter(), map(), and zip() require FusedIterator for correctness guaranteesfuse() on any iterator to wrap it in a guaranteed-fused adapterCode Example
use std::iter::FusedIterator;
struct Countdown { n: i32 }
impl Iterator for Countdown {
type Item = i32;
fn next(&mut self) -> Option<i32> {
if self.n <= 0 { return None; }
let val = self.n;
self.n -= 1;
Some(val)
}
}
// Marker trait - enforces the guarantee
impl FusedIterator for Countdown {}Key Differences
Seq.Nil terminates the sequence structurally (you can't call it again); Rust requires an explicit contract via FusedIterator.peekable() and chain() explicitly document whether they require or provide FusedIterator.fuse() adapter**: Rust provides Iterator::fuse() to wrap any iterator in a fused version — FusedIterator is the opt-in promise, fuse() is the runtime enforcement.FusedIterator types allow adapter code to skip the "check if already exhausted" bookkeeping.OCaml Approach
OCaml's Seq type is inherently fused — once a sequence node is Nil, there is no way to call next again (sequences are values, not objects). The concept of "calling next after None" does not apply to OCaml's immutable sequence type:
(* OCaml Seq terminates structurally — the empty sequence is just Seq.empty *)
let seq = Seq.empty (* calling next "again" is impossible by construction *)
Full Source
#![allow(clippy::all)]
//! # FusedIterator for Terminated Sequences
//!
//! `FusedIterator` guarantees: once `next()` returns `None`, all future calls also return `None`.
use std::iter::FusedIterator;
/// A properly fused countdown iterator
pub struct Countdown {
n: i32,
}
impl Countdown {
pub fn new(n: i32) -> Self {
Countdown { n }
}
}
impl Iterator for Countdown {
type Item = i32;
fn next(&mut self) -> Option<i32> {
if self.n <= 0 {
return None;
}
let val = self.n;
self.n -= 1;
Some(val)
}
}
// Declare that Countdown is fused — once None, always None
impl FusedIterator for Countdown {}
/// A badly-behaved iterator that returns Some after None (not fused)
/// This is an anti-pattern - don't do this in real code
pub struct FlickyIter {
count: i32,
}
impl FlickyIter {
pub fn new() -> Self {
FlickyIter { count: 0 }
}
}
impl Default for FlickyIter {
fn default() -> Self {
Self::new()
}
}
impl Iterator for FlickyIter {
type Item = i32;
fn next(&mut self) -> Option<i32> {
self.count += 1;
if self.count == 3 {
None // returns None once...
} else if self.count < 6 {
Some(self.count) // then Some again! (not fused)
} else {
None
}
}
}
/// Wraps any iterator to guarantee fused behavior
/// (This is what `.fuse()` does internally)
pub struct FusedWrapper<I> {
inner: Option<I>,
}
impl<I> FusedWrapper<I> {
pub fn new(iter: I) -> Self {
FusedWrapper { inner: Some(iter) }
}
}
impl<I: Iterator> Iterator for FusedWrapper<I> {
type Item = I::Item;
fn next(&mut self) -> Option<Self::Item> {
match &mut self.inner {
Some(iter) => match iter.next() {
Some(val) => Some(val),
None => {
self.inner = None; // Clear on first None
None
}
},
None => None, // Stay None forever
}
}
}
impl<I: Iterator> FusedIterator for FusedWrapper<I> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_countdown_fused() {
let mut cd = Countdown::new(3);
assert_eq!(cd.next(), Some(3));
assert_eq!(cd.next(), Some(2));
assert_eq!(cd.next(), Some(1));
assert_eq!(cd.next(), None);
assert_eq!(cd.next(), None); // stays None
}
#[test]
fn test_collect_countdown() {
let result: Vec<i32> = Countdown::new(5).collect();
assert_eq!(result, vec![5, 4, 3, 2, 1]);
}
#[test]
fn test_flicky_is_not_fused() {
let mut flicky = FlickyIter::new();
assert_eq!(flicky.next(), Some(1));
assert_eq!(flicky.next(), Some(2));
assert_eq!(flicky.next(), None); // Returns None
assert_eq!(flicky.next(), Some(4)); // But then Some again!
}
#[test]
fn test_fuse_adapter() {
let mut it = FlickyIter::new().fuse();
assert_eq!(it.next(), Some(1));
assert_eq!(it.next(), Some(2));
assert_eq!(it.next(), None);
assert_eq!(it.next(), None); // .fuse() guarantees this
}
#[test]
fn test_custom_fused_wrapper() {
let mut wrapped = FusedWrapper::new(FlickyIter::new());
assert_eq!(wrapped.next(), Some(1));
assert_eq!(wrapped.next(), Some(2));
assert_eq!(wrapped.next(), None);
assert_eq!(wrapped.next(), None); // Our wrapper also guarantees this
}
#[test]
fn test_std_vec_is_fused() {
let v = vec![1i32, 2, 3];
let mut it = v.into_iter();
assert_eq!(it.next(), Some(1));
assert_eq!(it.next(), Some(2));
assert_eq!(it.next(), Some(3));
assert_eq!(it.next(), None);
assert_eq!(it.next(), None); // std iterators are fused
}
#[test]
fn test_countdown_sum() {
let sum: i32 = Countdown::new(4).sum();
assert_eq!(sum, 4 + 3 + 2 + 1);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_countdown_fused() {
let mut cd = Countdown::new(3);
assert_eq!(cd.next(), Some(3));
assert_eq!(cd.next(), Some(2));
assert_eq!(cd.next(), Some(1));
assert_eq!(cd.next(), None);
assert_eq!(cd.next(), None); // stays None
}
#[test]
fn test_collect_countdown() {
let result: Vec<i32> = Countdown::new(5).collect();
assert_eq!(result, vec![5, 4, 3, 2, 1]);
}
#[test]
fn test_flicky_is_not_fused() {
let mut flicky = FlickyIter::new();
assert_eq!(flicky.next(), Some(1));
assert_eq!(flicky.next(), Some(2));
assert_eq!(flicky.next(), None); // Returns None
assert_eq!(flicky.next(), Some(4)); // But then Some again!
}
#[test]
fn test_fuse_adapter() {
let mut it = FlickyIter::new().fuse();
assert_eq!(it.next(), Some(1));
assert_eq!(it.next(), Some(2));
assert_eq!(it.next(), None);
assert_eq!(it.next(), None); // .fuse() guarantees this
}
#[test]
fn test_custom_fused_wrapper() {
let mut wrapped = FusedWrapper::new(FlickyIter::new());
assert_eq!(wrapped.next(), Some(1));
assert_eq!(wrapped.next(), Some(2));
assert_eq!(wrapped.next(), None);
assert_eq!(wrapped.next(), None); // Our wrapper also guarantees this
}
#[test]
fn test_std_vec_is_fused() {
let v = vec![1i32, 2, 3];
let mut it = v.into_iter();
assert_eq!(it.next(), Some(1));
assert_eq!(it.next(), Some(2));
assert_eq!(it.next(), Some(3));
assert_eq!(it.next(), None);
assert_eq!(it.next(), None); // std iterators are fused
}
#[test]
fn test_countdown_sum() {
let sum: i32 = Countdown::new(4).sum();
assert_eq!(sum, 4 + 3 + 2 + 1);
}
}
Deep Comparison
OCaml vs Rust: FusedIterator
Pattern 1: Guaranteed Termination
OCaml
(* OCaml Seq.t is naturally fused by convention *)
let countdown n =
Seq.unfold (fun i -> if i <= 0 then None else Some (i, i-1)) n
(* Once None, stays None - but convention, not enforced *)
Rust
use std::iter::FusedIterator;
struct Countdown { n: i32 }
impl Iterator for Countdown {
type Item = i32;
fn next(&mut self) -> Option<i32> {
if self.n <= 0 { return None; }
let val = self.n;
self.n -= 1;
Some(val)
}
}
// Marker trait - enforces the guarantee
impl FusedIterator for Countdown {}
Pattern 2: Safety Wrapper
Rust
// If you don't trust an iterator, wrap it
let safe = weird_iterator.fuse();
// Now guaranteed: once None, always None
Pattern 3: All Standard Iterators
Rust
let mut v = vec![1, 2, 3].into_iter();
v.next(); // Some(1)
v.next(); // Some(2)
v.next(); // Some(3)
v.next(); // None
v.next(); // None - all std iterators are fused
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Guarantee | Convention only | FusedIterator marker trait |
| Enforcement | Not by type system | Compiler-checked |
| Standard library | All well-behaved | All implement FusedIterator |
| Safety wrapper | N/A | .fuse() adapter |
| Implementation | Programmer discipline | impl FusedIterator for T {} |
Exercises
Some after None) and demonstrate the incorrect behavior, then fix it by fusing.iterator.fuse() wraps the iterator in a type that implements FusedIterator regardless of whether the original did.FusedIterator on a bounded random-number generator that stops after generating exactly N values.