The Problem

Every Rust programmer eventually writes a loop that pushes items into a Vec, HashMap, or a custom collection. The code works, but it feels verbose and sometimes allocates more than necessary. I’ve seen this pattern in production services where a hot path builds a lookup table from a stream of database rows.

Why FromIterator Matters

FromIterator is the trait that powers collect(). When a type implements it, the compiler can allocate the exact capacity up front (if the iterator reports a size hint) and avoid intermediate buffers. This turns a manual push loop into a single, optimized allocation — often a measurable win in latency‑sensitive code.

Tip: If your iterator is ExactSizeIterator, collect() will pre‑allocate the exact number of elements.

A Real‑World Example

Imagine a service that ingests JSON logs, extracts a (user_id, timestamp) pair, and builds a HashMap<u64, Vec<i64>> grouping timestamps per user. The naïve version looks like this:

fn group_by_user_naive(logs: &[LogEntry]) -> HashMap> {
    let mut map = HashMap::new();
    for entry in logs {
        map.entry(entry.user_id)
           .or_default()
           .push(entry.timestamp);
    }
    map
}

It works, but each or_default may allocate a new Vec and the map may rehash repeatedly. Using FromIterator we can express the same logic as a single collect:

use std::collections::HashMap;
use std::iter::FromIterator;

fn group_by_user(logs: &[LogEntry]) -> HashMap> {
    logs.iter()
        .map(|e| (e.user_id, e.timestamp))
        .collect::>>()
}

Because HashMap implements FromIterator<(K, V)> where V: FromIterator<T>, the compiler builds each inner Vec with the exact number of timestamps for that user, and the outer map reserves capacity based on the iterator’s size hint.

Putting It Together

Here’s a production‑ready helper that works for any iterator of key‑value pairs, returning a HashMap<K, Vec<V>> with zero‑cost abstractions:

pub fn group_by(iter: I) -> HashMap>
where
    K: Eq + Hash,
    I: IntoIterator,
{
    iter.into_iter().collect()
}

Usage stays clean:

let grouped = group_by(logs.iter().map(|e| (e.user_id, e.timestamp)));

Gotchas and Tips

  • Size hints matter. If you feed a plain Vec slice, the iterator knows its length. Chaining .filter() or .flat_map() discards the hint, causing a fallback to exponential growth.
  • Custom collectors. For types that don’t implement FromIterator (e.g., a ring buffer), write a small wrapper that implements the trait — you’ll still get the ergonomic collect() call.
  • Parallelism. rayon’s ParallelIterator also provides collect() via FromParallelIterator, giving the same allocation benefits across threads.

Next time you reach for a mutable map and a loop, pause and ask whether the data already flows through an iterator. A single collect() often replaces twenty lines of boilerplate, reduces allocations, and makes the intent obvious to the next maintainer — which, in my experience, is the real win.