Skip to content

← Closures and Iterators step 21 of 28

Medium Primitives

collect and the turbofish: three targets, one source

Build three outputs from the same input numbers:

pub fn shapes(nums: Vec<i64>) -> (Vec<i64>, String, Vec<Vec<i64>>)
  • The first output contains each number multiplied by two.
  • The second contains one ASCII digit per number: its last decimal digit, ignoring its sign. For example, 12, -7 and 5 contribute 275.
  • The third contains the original numbers in groups of three. Keep a final group of one or two numbers when the length is not divisible by three.

Preserve input order and duplicates in all outputs. Empty input returns ([], "", []); it does not produce an empty inner group. Inputs are in i64::MIN / 2 ..= i64::MAX / 2, so every doubled value fits in i64.

[12, -7, 5]  -> ([24, -14, 10], "275",  [[12, -7, 5]])
[1, 2, 3, 4] -> ([2, 4, 6, 8],  "1234", [[1, 2, 3], [4]])

Choose what collect builds

collect consumes an iterator and builds a result whose type implements FromIterator for the iterator's items. The items constrain the choices, but do not always select one result type. Characters, for example, can be collected into either a String or a Vec<char>.

The target type can come from a binding annotation, explicit generic arguments or surrounding context such as a function's return type. Here are all three:

fn doubled(values: &[i64]) -> Vec<i64> {
    values.iter().map(|n| n * 2).collect()
}

fn main() {
    let values = [1i64, 2, 3];
    let annotated: Vec<i64> = values.iter().map(|n| n * 2).collect();
    let explicit = values.iter().map(|n| n * 2).collect::<Vec<_>>();
    assert_eq!(annotated, explicit);
    assert_eq!(doubled(&values), [2, 4, 6]);
}

The syntax ::<...> is commonly called the turbofish. In collect::<Vec<_>>(), Vec chooses the destination and _ asks Rust to infer its element type. That works when the remaining constraints determine the element type. An explicit Vec<i64> is also useful when you want to state that requirement; the shortest annotation is not always the clearest one.

Why the starter is ambiguous

The starter collects characters into digit_chars without selecting its type, then passes that value to String::from_iter. This still leaves multiple possible intermediate collections, so the compiler reports E0283.

Choose an intermediate type, or avoid the intermediate collection and collect the characters directly into a String. The latter is a simpler route here. The other collections can obtain their types from the returned tuple, though annotations can make the code easier to read.

E0282 and E0283 both concern insufficient type information. Their diagnostic notes describe different inference problems; they do not imply fundamentally different repair strategies. Read the expression and required trait bounds, then provide the missing type information where it is most helpful.

One character for each last digit

Use n.unsigned_abs() % 10 to obtain a digit magnitude in 0..=9. Convert it to an ASCII digit with char::from(b'0' + digit as u8). The cast is safe here because the remainder is at most nine.

The magnitude operation also works at i64::MIN, unlike signed abs(). This standalone example demonstrates that property; i64::MIN itself is outside this problem's doubling contract:

fn main() {
    let values = [12i64, -7, 100, i64::MIN];
    let digits: String = values.iter()
        .map(|n| char::from(b'0' + (n.unsigned_abs() % 10) as u8))
        .collect();
    assert_eq!(digits, "2708");
}

Turn borrowed chunks into owned groups

nums.chunks(3) yields borrowed slices. Map each slice to an owned vector with to_vec(), then collect those vectors. For these i64 elements, this copies the values into the new groups.

fn main() {
    let values = [1i64, 2, 3, 4];
    let groups: Vec<Vec<i64>> = values.chunks(3).map(|chunk| chunk.to_vec()).collect();
    assert_eq!(groups, [vec![1, 2, 3], vec![4]]);

}

A positive chunk size produces nonempty chunks, with no chunks for an empty slice. A zero chunk size panics. chunks_exact(3) only yields full groups, retaining a short remainder for separate access; it would miss this task's final group unless you handled that remainder explicitly.

For a slice, Clippy's iter_cloned_collect lint can suggest to_vec() instead of .iter().cloned().collect(). Here, to_vec() directly expresses the desired conversion from a borrowed chunk to an owned group.