Skip to content

← Closures and Iterators step 20 of 28

Medium Primitives

partition and unzip: two collections from one pass

Take a list of (name, score) rows and produce three things.

pub fn split_scores(
    entries: Vec<(String, i64)>,
    cutoff: i64,
) -> (Vec<String>, Vec<String>, Vec<i64>)
  • .0 — the names whose score is at least cutoff;
  • .1 — the names whose score is below cutoff;
  • .2all the scores, in the original input order.
entries = [("a", 5), ("b", 1), ("c", 9)], cutoff = 5
  -> (["a", "c"], ["b"], [5, 1, 9])

Both partitions preserve input relative order. Say that back to yourself, because most people assume the opposite: partition is not a sort, does not reorder, and does not group. It walks once and pushes each element onto one of two piles, so within each pile the original order is intact. The hidden cases check it.

Two consumers that build more than one thing

Almost every consumer you have met so far returns one value. These two return a pair, and that changes what inference can do for you.

fn unzip<A, B, FromA, FromB>(self) -> (FromA, FromB)
where Self: Iterator<Item = (A, B)>,
      FromA: Default + Extend<A>,
      FromB: Default + Extend<B>;

fn partition<B, F>(self, f: F) -> (B, B)
where B: Default + Extend<Self::Item>,
      F: FnMut(&Self::Item) -> bool;

unzip takes an iterator of pairs and gives you two collections. partition takes a predicate and gives you two collections of the same type — true first, false second. Both make exactly one pass.

Why the starter does not compile

Remove the annotations and you get:

error[E0282]: type annotations needed for `(_, _)`

Look at the bounds again. Neither function’s return type is determined by its input: FromA only has to be Default + Extend<A>, and Vec<A>, VecDeque<A>, BTreeSet<A> and String (for A = char) all qualify. Nothing in the expression picks one. This is return-type-driven inference, the same mechanism as collect and parse, and it means you have to say what you want:

let (names, scores): (Vec<String>, Vec<i64>) = entries.into_iter().unzip();

unzip needs both halves inferable. partition needs its single collection type known — a turbofish works too: .partition::<Vec<_>, _>(..), though the binding annotation is usually easier to read.

Extend is the thing underneath

Both bounds mention it, and it is worth a minute:

pub trait Extend<A> {
    fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T);
}

FromIterator (what collect uses) builds a new collection. Extend appends to an existing one. That distinction is the whole reason partition and unzip can be written generically at all: they create two empty collections with Default and then push into them with Extend.

It is also directly useful. v.extend(other_iter) avoids the intermediate Vec that v.append(&mut other.collect()) would build, and it accepts any IntoIterator, so v.extend(a.iter().copied()) and v.extend([1, 2, 3]) both work. When you catch yourself writing a for loop whose whole body is v.push(x), that is extend.

type_complexity will bite you

Partitioning pairs means annotating a pair-of-vectors-of-pairs, and clippy has a default-on opinion about that:

error: very complex type used. Consider factoring parts into `type` definitions
       [clippy::type_complexity]

The fix is a type alias, and it is a genuine readability win rather than lint-appeasement:

type Scored = Vec<(String, i64)>;
let (pass, fail): (Scored, Scored) = ...;

You will meet this lint again in item 9.23, where chained adapter types get much worse than this.

Order of operations

You need the scores twice — once to decide the partition, once to return them whole and in order. Unzip first, then zip the names back against the scores by value (scores.iter().copied()), partition that, and strip the scores off each half afterwards. The original scores vector is untouched and is your third return value.

Grade is compile + tests + clippy -D warnings.

Loading visualization…