Skip to content

← Closures and Iterators step 1 of 28

Easy Primitives

Closures 101: build a pipeline of boxed closures

Build a pipeline of boxed callables, then apply it to each input number.

pub fn apply_pipeline(nums: Vec<i64>, ops: Vec<String>) -> Vec<i64>

Read operation names from left to right. Recognize these exact, case-sensitive strings and skip every other string without trimming or changing its case:

Name Operation on the current value x
double x * 2
inc x + 1
dec x - 1
neg -x
square x * x
abs Absolute value of x

Apply every recognized operation, including repetitions, to each number in order. Preserve the order and length of nums. Empty input produces empty output; an empty list of operations, or a list containing only unknown names, leaves the numbers unchanged.

For [1, 2, 3] and ["double", "inc"], return [3, 5, 7]. Reversing those operations gives [4, 6, 8].

Inputs to this exercise keep every intermediate result within i64. In particular, neg and abs are not applied to i64::MIN, and multiplication must fit before the next operation runs. No wrapping or saturating arithmetic is requested.

Use a Vec<Box<dyn Fn(i64) -> i64>> for this exercise. The output tests check values; they do not enforce that representation. The purpose of the boxed representation is to practice storing callables with different concrete types.

Give the closures a common type

Each closure expression has its own anonymous type, even when two expressions have identical bodies. The starter returns different closure types inside Some(...) in its match arms, without a common target type for coercion. That produces E0308, a type mismatch.

A box containing a callable can coerce to Box<dyn Fn(i64) -> i64> when that target type is known. Here is a complete example:

fn main() {
    let pipeline: Vec<Box<dyn Fn(i64) -> i64>> = vec![
        Box::new(|x| x * 2),
        Box::new(|x| x + 1),
    ];
    let result = pipeline.iter().fold(3, |value, op| op(value));
    assert_eq!(result, 7);
}

If you build the list with filter_map, give its closure an explicit return type: |name| -> Option<Box<dyn Fn(i64) -> i64>> { ... }. Return None for an unknown name and Some(Box::new(...)) for a recognized operation. This gives each match arm a place to perform the coercion.

Function items also implement the callable traits. For absolute value, use Box::new(i64::abs). The function still needs Box::new; it does not turn directly into a box through coercion alone.

Boxing is one choice

These operations do not capture any variables, so they can also coerce to function pointers. This is a valid alternative representation outside the exercise's boxed-callable requirement:

fn main() {
    let pipeline: [fn(i64) -> i64; 2] = [|x| x * 2, |x| x + 1];
    assert_eq!(pipeline.iter().fold(3, |value, op| op(value)), 7);
}

A capturing closure cannot use that function-pointer conversion. Generic parameters and borrowed callable trait objects are other useful API choices. Choosing a function at runtime does not inherently require a heap allocation. Even Box::new needs no allocation for a zero-sized value, such as a noncapturing closure; the pipeline vector still needs storage for its entries.

Inference and captured values

Closure parameter types can come from annotations, expected types and uses. An identity closure |x| x does not become generic over unrelated argument types: using the same closure with both an integer and a string is a mismatch. A generic function fn identity<T>(x: T) -> T can support those separate calls.

Capturing data and calling the closure are separate concerns. move puts captured values into the closure, copying them when their types are Copy. It does not by itself mean the closure can only be called once:

fn main() {
    let label = String::from("step");
    let length = move || label.len();
    assert_eq!(length(), 4);
    assert_eq!(length(), 4);
}

This closure owns its string but only reads it when called, so it implements Fn. A closure that changes captured state may need FnMut; one that moves a captured non-Copy value out when called can only be called through FnOnce. The operations in this pipeline capture nothing and can all be called via Fn.

Clippy's redundant_closure lint can suggest passing a function directly when a closure only forwards its arguments. For example, i64::abs avoids the wrapper |x| x.abs(). Such suggestions concern a particular expression; they do not mean every closure wrapper is unnecessary.