Skip to content

← Trait Objects and Dispatch step 1 of 10

Medium Primitives

Box<dyn Trait>: trait objects, DSTs and dynamic dispatch

A vector has one element type. Trait objects let that type be a pointer to values with different implementations of a shared interface. Here you will build an ordered list of arithmetic operations and compose callable values.

Your task

The starter provides Op and the Add, Mul and Clamp implementations. Complete these functions, keeping the required signatures:

pub fn build(spec: &[(String, i64)]) -> Vec<Box<dyn Op>>
pub fn compose(fns: Vec<Box<dyn Fn(i64) -> i64>>) -> Box<dyn Fn(i64) -> i64>
pub fn pipeline(ops: Vec<(String, i64)>, start: i64) -> i64

build creates an operation for each recognized entry, preserving order and duplicates:

Name Operation with parameter n
add x + n
mul x * n
clamp x.min(n), an upper limit only

Names are case-sensitive. Skip every other name; it contributes no operation. An empty or entirely unrecognized specification produces an empty vector.

pipeline calls build, then passes start through the operations in order. With [("add", 5), ("mul", 3)] and start = 2, the result is 21. With no operations, return start unchanged.

compose returns a callable that applies its inputs left to right: compose([f, g])(x) means g(f(x)). An empty list returns an identity callable. The returned value must support repeated calls, as its Fn type requires. Its captured operations remain available for those calls.

Inputs and calls stay within i64 arithmetic bounds at every intermediate step, and operation lists are small enough for the time and memory limits. The harness also checks composition with x + 1, then x * 2, then x - 3, and separately checks an empty composition. Implement general composition, rather than only that particular sequence.

Why the starter's vector type fails

The starter returns Vec<dyn Op>. dyn Op is dynamically sized: its type alone does not determine the size of the hidden concrete value. Vec<T> requires a sized element type, so this produces E0277.

Box<dyn Op> is sized and owns the value it points to. All elements of Vec<Box<dyn Op>> therefore have the same pointer type, while their owned values may be Add, Mul or Clamp. Annotate the vector's type so that boxes of those different concrete types can be coerced to the shared trait object type.

Ordinary generic type parameters have an implicit Sized bound unless it is relaxed with ?Sized. Trait Self does not have that implicit bound. Writing ?Sized on a helper's parameter does not change Vec's element requirements.

Ownership and dispatch are separate choices

This task requires owned boxes. Trait objects can also be borrowed through &dyn Op or shared through a suitable owning pointer such as Rc<dyn Op>. Box is not required for every use of dynamic dispatch.

A trait-object pointer carries access to the data and metadata for calling the hidden type's methods through a vtable. This permits runtime dispatch. An optimizer may simplify a particular call when its concrete target is known, so source-level dynamic dispatch does not promise an indirect machine call in every optimized build.

Treat the common two-word pointer model as an implementation detail, not a stable layout or serialization contract. See the trait-object description and pointer layout guidance.

For a known, fixed set of operation variants, an enum is another possible design. Generic code can operate on that enum. Generics and trait objects solve different representation problems; neither is always faster or more useful.

Store the functions, then call them in order

Distinct closure expressions have distinct anonymous types. Boxing them as dyn Fn(i64) -> i64 gives the vector a common element type and supports closures that capture owned values.

Trait objects are not the only way to store callables. For example, closures that capture nothing can become function pointers:

fn main() {
    let functions: Vec<fn(i64) -> i64> = vec![|x| x + 1, |x| x * 2];
    let result = functions.iter().fold(3, |value, function| function(value));
    assert_eq!(result, 8);
}

Keep the boxed Fn API required by this exercise. You can return one closure that owns the vector and loops over it, or build a chain of closures. Use move to transfer captured ownership into the returned closure. A chain adds nested calls and nested captured values; a single loop avoids that growing call depth.

Read diagnostics in context

Use dyn Op when naming a trait object; Box<Op> is rejected in this edition. An unboxed dyn Op return value also lacks a statically known size. Returning impl Op is a different API: it chooses one concrete return type, so it is not a direct replacement for this heterogeneous collection.

Clippy's vec_box lint does not reject this Vec<Box<dyn Op>>. For other vectors, consider why a box is present before following or suppressing a suggestion. A sized element does not make boxing inherently pointless; ownership and address requirements can matter too.