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], aHashSet<T>, aBTreeSet<T>, anOption<T>(which is a 0-or-1 elementIntoIterator!), 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)!