Advanced Rust Design Patterns

Iterator-Based API Design

Designing public APIs around the Iterator and IntoIterator traits—both for accepting sequences as inputs and for returning lazy sequences as outputs—eliminates forced intermediate Vec<T> heap allocations and allows callers to compose, short-circuit (.take(5), .any(...)), or collect results into any container they choose.

 Rigid Vec-Based API (Forces 2 Heap Allocations!):
   `fn filter_valid(inputs: Vec<String>) -> Vec<String>`
   Caller must allocate a `Vec` to call it, and callee must allocate a `Vec` to return!

 Ergonomic Iterator-Based API (0 Mandatory Heap Allocations!):
   `fn filter_valid<I>(inputs: I) -> impl Iterator<Item = String>`
   `where I: IntoIterator<Item = String>`

1. Accepting IntoIterator Instead of &[T] or Vec<T>

When a function needs to consume or iterate over a sequence of items without requiring random index access (slice[i]), accept I: IntoIterator<Item = ...>:

  • Callers can pass a Vec<T>, an array [a, b, c], a HashSet<T>, a BTreeSet<T>, an Option<T> (which is a 0-or-1 element IntoIterator!), or a lazy .map(...).filter(...) adapter chain directly!

2. Returning Lazy Sequences: -> impl Iterator vs Custom Named Struct

When returning a lazy sequence from a public function, you have two choices:

Return Type Pros Cons
-> impl Iterator<Item = T> Concise; hides internal .map(...).filter(...) adapter chain; captures all in-scope lifetimes automatically in Rust 2024. Cannot be named in a struct field or trait signature without TAIT; doesn't automatically forward extra traits like Clone or Debug unless added to bounds (+ Clone).
Custom Named Struct (pub struct Tokens<'a>) Can implement DoubleEndedIterator, ExactSizeIterator, FusedIterator, Clone, and Debug, and can be stored in caller structs by name! Requires a ~15-line struct + impl Iterator block.
use std::iter::FusedIterator;

/// 1. Accepting `IntoIterator`: works with arrays, Vecs, HashSets, Options, or adapter chains!
pub fn join_non_empty<I, S>(items: I, sep: &str) -> String
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let mut out = String::new();
    for item in items {
        let s = item.as_ref().trim();
        if !s.is_empty() {
            if !out.is_empty() {
                out.push_str(sep);
            }
            out.push_str(s);
        }
    }
    out
}

/// 2. Returning a custom named iterator struct (`KeyValueIter<'a>`) that implements
///    `Iterator` and `FusedIterator` with zero heap allocations!
#[derive(Debug, Clone)]
pub struct KeyValueIter<'a> {
    remaining: &'a str,
}

pub fn parse_key_values(input: &str) -> KeyValueIter<'_> {
    KeyValueIter { remaining: input }
}

impl<'a> Iterator for KeyValueIter<'a> {
    type Item = (&'a str, &'a str);

    fn next(&mut self) -> Option<Self::Item> {
        while !self.remaining.is_empty() {
            let (line, rest) = self
                .remaining
                .split_once('\n')
                .unwrap_or((self.remaining, ""));
            self.remaining = rest;

            let trimmed = line.trim();
            if trimmed.is_empty() || trimmed.starts_with('#') {
                continue;
            }
            if let Some((k, v)) = trimmed.split_once('=') {
                return Some((k.trim(), v.trim()));
            }
        }
        None
    }
}

impl FusedIterator for KeyValueIter<'_> {}

fn main() {
    // Pass a fixed array, an Option, or a filtered iterator directly into `join_non_empty`:
    assert_eq!(join_non_empty(["  alpha ", "", "beta", "gamma "], "::"), "alpha::beta::gamma");
    assert_eq!(join_non_empty(Some("single_item"), ","), "single_item");

    let config_text = "# Server settings\nhost = 127.0.0.1\nport = 8080\n";
    let pairs: Vec<(&str, &str)> = parse_key_values(config_text).collect();
    assert_eq!(pairs, vec![("host", "127.0.0.1"), ("port", "8080")]);

    println!("Joined: {}, parsed pairs: {pairs:?}", join_non_empty(["a", "b"], "-"));
}
Joined: a-b, parsed pairs: [("host", "127.0.0.1"), ("port", "8080")]

Streaming Fallible Sequences (Iterator<Item = Result<T, E>>)

Because Result<V, E> implements FromIterator<Result<T, E>> where V: FromIterator<T>, returning Iterator<Item = Result<T, E>> lets callers use .collect::<Result<Vec<T>, E>>() to collect all Ok items into a Vec<T> while short-circuiting immediately on the first Err(e)!