Skip to content

← Closures and Iterators step 21 of 28

Medium Primitives

collect and the turbofish: three targets, one source

Build three different collections from the same numbers.

pub fn shapes(nums: Vec<i64>) -> (Vec<i64>, String, Vec<Vec<i64>>)
  • .0 — every number doubled, as a Vec<i64>.
  • .1 — a String made of each number’s last decimal digit, in order, ignoring the sign. [12, -7, 5] gives "275".
  • .2 — the original numbers in chunks of three, in order; the last chunk may be shorter.
[12, -7, 5]  ->  ([24, -14, 10], "275",  [[12, -7, 5]])
[1, 2, 3, 4] ->  ([2, 4, 6, 8],  "1234", [[1, 2, 3], [4]])
[]           ->  ([],            "",     [])

collect runs backwards

Every other method you have met takes its meaning from what is on the left of the dot. collect takes its meaning from what is on the right of the equals sign — from the type you are assigning into.

pub fn collect<B: FromIterator<Self::Item>>(self) -> B

B appears only in the return type. Nothing about the iterator picks it. So collect is not “make a Vec”; it is “make whatever the context asked for, provided that thing knows how to be built from these items”. With Item = char alone, the candidates in alloc include String, Box<str> and Vec<char>; with Item = (K, V), HashMap<K, V> and BTreeMap<K, V>; with Item = Result<T, E>, the wonderful Result<Vec<T>, E> that stops at the first error.

This is return-type-driven inference. Outside Rust and Haskell it is rare, and until someone explains the mechanism the error looks like a compiler malfunction.

The error, and which code it is

The starter collects into a binding whose type nothing determines:

error[E0283]: type annotations needed
  |
4 |     let digit_chars = nums
  |         ^^^^^^^^^^^
...
7 |         .collect();
  |          ------- type must be known at this point
  |
  = note: multiple `impl`s satisfying `_: FromIterator<char>` found in the `alloc` crate:
          - impl FromIterator<char> for Box<str>;
          - impl FromIterator<char> for ByteString;
          - impl FromIterator<char> for String;
help: consider giving `digit_chars` an explicit type

Note the code: E0283, not E0282. The difference matters and is worth learning, because the fix is different.

  • E0282 is “I have no information at all”. Nothing constrains the type.
  • E0283 is “I have several candidates and no way to choose”. That is what happened here — the compiler knows the items are char and can list three types that accept them.

When you see the “multiple impls satisfying” note, you are being asked to pick, not to inform.

Three ways to say it, and which to prefer

let v: Vec<i64> = it.collect();          // annotate the binding
let v = it.collect::<Vec<i64>>();        // turbofish
fn f() -> Vec<i64> { it.collect() }      // let the return type say it

All three work. The habit worth building is the minimal annotation:

it.collect::<Vec<_>>()

The _ says “you work out the element type” — which the compiler always can, from the iterator — while the Vec says “this is the container”, which it cannot. Writing collect::<Vec<i64>>() is not wrong, but it duplicates information that already exists and will need editing when the element type changes.

The name turbofish is for ::<>, and it exists because f<T>(x) would be ambiguous with f < T > (x) in an expression. The :: disambiguates. Once you know that, the syntax stops looking arbitrary.

In this problem

Three collects, three different targets, and the third one nests: a Vec<Vec<i64>> built from slice::chunks, whose items are &[i64] and therefore need to_vec() before they can go in. chunks(n) never yields an empty chunk and never panics for n > 0; the final chunk is short if the length does not divide evenly. (chunks_exact drops the remainder instead, and hands it to you separately via remainder().)

For the digit character: n.unsigned_abs() gives the magnitude as a u64 without the i64::MIN overflow that abs() would have, % 10 takes the last digit, and char::from(b'0' + d) turns 0..=9 into '0'..='9'.

Two lints to know here

  • iter_cloned_collect.iter().cloned().collect() on a slice is .to_vec(). Default-on.
  • from_iter_instead_of_collectString::from_iter(it) is it.collect::<String>(). This one is pedantic, so it will not fire here, but the starter is written in exactly that style and the postfix form reads better in a chain.

Grade is compile + tests + clippy -D warnings.

Loading visualization…