ExamplesBy LevelBy TopicLearning Paths
123 Intermediate

impl Trait in Function Signatures

Functional Programming

Tutorial

The Problem

Before impl Trait, returning a closure or a complex iterator chain from a Rust function required either boxing (Box<dyn Fn...>) with a heap allocation, or writing out the unnameable concrete type by hand — impossible for iterator chains and closures. impl Trait in return position solves this: the function promises to return "some type implementing this trait" without naming it, enabling zero-allocation returns of closures and iterator pipelines while hiding implementation details from callers.

🎯 Learning Outcomes

  • • Distinguish impl Trait in argument position (sugar for generics) from return position (opaque type)
  • • Understand why opaque return types avoid heap allocation for closures and iterators
  • • See how returning impl Iterator<Item = T> enables lazy, composable pipelines
  • • Learn when Box<dyn Trait> is preferable over impl Trait (heterogeneous collections, dynamic dispatch)
  • Code Example

    // Explicit trait bound; compiler monomorphizes per call site
    pub fn stringify_all(items: &[impl Display]) -> Vec<String> {
        items.iter().map(|x| x.to_string()).collect()
    }

    Key Differences

  • Granularity: OCaml hides types at the module boundary; Rust's impl Trait hides them at the individual function level.
  • Allocation: Rust impl Fn in return position avoids heap allocation; OCaml closures are always heap-allocated.
  • Multiple callers: Rust's opaque type is a single concrete type per function — not a union of possibilities; OCaml's abstract type is similarly fixed per module instantiation.
  • Dynamic dispatch: When the concrete type varies at runtime, Rust uses Box<dyn Trait>; OCaml uses first-class modules or polymorphic variants.
  • OCaml Approach

    OCaml does not have an equivalent to opaque return types. Functions return concrete types, and module signatures provide abstraction by hiding the implementation. module type S = sig type t val f : int -> t end is the OCaml mechanism for hiding a concrete type behind an interface — analogous to Rust's impl Trait in return position, but at the module level rather than the function level.

    Full Source

    #![allow(clippy::all)]
    //! Example 123: impl Trait in Function Signatures
    //!
    //! `impl Trait` in argument position: syntactic sugar for generics —
    //! the compiler monomorphizes one concrete type per call site.
    //!
    //! `impl Trait` in return position: opaque return type — the caller sees
    //! only the trait bound; the concrete type is hidden and chosen by the
    //! function body. This lets you return unnameable types like closures and
    //! complex iterator chains without heap-boxing them.
    
    use std::fmt::Display;
    
    // ---------------------------------------------------------------------------
    // Approach 1: impl Trait in argument position
    // Equivalent to fn stringify_all<T: Display>(items: &[T]) -> Vec<String>
    // ---------------------------------------------------------------------------
    
    pub fn stringify_all(items: &[impl Display]) -> Vec<String> {
        items.iter().map(|x| x.to_string()).collect()
    }
    
    // Generic version — identical semantics, more explicit syntax
    pub fn stringify_all_generic<T: Display>(items: &[T]) -> Vec<String> {
        items.iter().map(|x| x.to_string()).collect()
    }
    
    // ---------------------------------------------------------------------------
    // Approach 2: impl Trait in return position (opaque return type)
    //
    // The concrete type (a closure `impl Fn(i32) -> i32`) is unnameable, so
    // we return `impl Fn(i32) -> i32` instead of boxing it with `Box<dyn Fn>`.
    // ---------------------------------------------------------------------------
    
    pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
        move |x| x + n
    }
    
    // ---------------------------------------------------------------------------
    // Approach 3: Returning an opaque iterator
    //
    // The concrete type of the chain below is unwritable by hand.
    // `impl Iterator<Item = u32>` lets the caller iterate without caring.
    // ---------------------------------------------------------------------------
    
    pub fn even_squares(limit: u32) -> impl Iterator<Item = u32> {
        (0..limit).filter(|n| n % 2 == 0).map(|n| n * n)
    }
    
    // ---------------------------------------------------------------------------
    // Approach 4: Multiple trait bounds in argument position
    // ---------------------------------------------------------------------------
    
    pub fn print_and_count(items: &[impl Display + std::fmt::Debug]) -> usize {
        items.len()
    }
    
    // ---------------------------------------------------------------------------
    // Tests
    // ---------------------------------------------------------------------------
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_stringify_ints() {
            assert_eq!(stringify_all(&[1, 2, 3]), vec!["1", "2", "3"]);
        }
    
        #[test]
        fn test_stringify_floats() {
            assert_eq!(
                stringify_all(&[1.5_f64, 2.5, 3.5]),
                vec!["1.5", "2.5", "3.5"]
            );
        }
    
        #[test]
        fn test_stringify_generic_same_as_impl_trait() {
            let a = stringify_all(&[10, 20, 30]);
            let b = stringify_all_generic(&[10, 20, 30]);
            assert_eq!(a, b);
        }
    
        #[test]
        fn test_make_adder_basic() {
            let add5 = make_adder(5);
            assert_eq!(add5(10), 15);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-3), 2);
        }
    
        #[test]
        fn test_make_adder_independent_closures() {
            let add3 = make_adder(3);
            let add7 = make_adder(7);
            assert_eq!(add3(10) + add7(10), 30);
        }
    
        #[test]
        fn test_even_squares_basic() {
            let result: Vec<u32> = even_squares(7).collect();
            // even numbers < 7: 0, 2, 4, 6  → squares: 0, 4, 16, 36
            assert_eq!(result, vec![0, 4, 16, 36]);
        }
    
        #[test]
        fn test_even_squares_empty() {
            let result: Vec<u32> = even_squares(0).collect();
            assert!(result.is_empty());
        }
    
        #[test]
        fn test_print_and_count() {
            assert_eq!(print_and_count(&[1, 2, 3, 4]), 4);
            assert_eq!(print_and_count(&["a", "b"]), 2);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_stringify_ints() {
            assert_eq!(stringify_all(&[1, 2, 3]), vec!["1", "2", "3"]);
        }
    
        #[test]
        fn test_stringify_floats() {
            assert_eq!(
                stringify_all(&[1.5_f64, 2.5, 3.5]),
                vec!["1.5", "2.5", "3.5"]
            );
        }
    
        #[test]
        fn test_stringify_generic_same_as_impl_trait() {
            let a = stringify_all(&[10, 20, 30]);
            let b = stringify_all_generic(&[10, 20, 30]);
            assert_eq!(a, b);
        }
    
        #[test]
        fn test_make_adder_basic() {
            let add5 = make_adder(5);
            assert_eq!(add5(10), 15);
            assert_eq!(add5(0), 5);
            assert_eq!(add5(-3), 2);
        }
    
        #[test]
        fn test_make_adder_independent_closures() {
            let add3 = make_adder(3);
            let add7 = make_adder(7);
            assert_eq!(add3(10) + add7(10), 30);
        }
    
        #[test]
        fn test_even_squares_basic() {
            let result: Vec<u32> = even_squares(7).collect();
            // even numbers < 7: 0, 2, 4, 6  → squares: 0, 4, 16, 36
            assert_eq!(result, vec![0, 4, 16, 36]);
        }
    
        #[test]
        fn test_even_squares_empty() {
            let result: Vec<u32> = even_squares(0).collect();
            assert!(result.is_empty());
        }
    
        #[test]
        fn test_print_and_count() {
            assert_eq!(print_and_count(&[1, 2, 3, 4]), 4);
            assert_eq!(print_and_count(&["a", "b"]), 2);
        }
    }

    Deep Comparison

    OCaml vs Rust: impl Trait

    Side-by-Side Code

    OCaml — polymorphic argument (parametric polymorphism)

    (* OCaml infers the most general type automatically *)
    let stringify_all to_s items = List.map to_s items
    
    (* Returning a function — concrete type is always visible in OCaml *)
    let make_adder n = fun x -> x + n
    

    Rust — impl Trait in argument position

    // Explicit trait bound; compiler monomorphizes per call site
    pub fn stringify_all(items: &[impl Display]) -> Vec<String> {
        items.iter().map(|x| x.to_string()).collect()
    }
    

    Rust — impl Trait in return position (opaque type)

    // Concrete closure type is hidden; zero heap allocation
    pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
        move |x| x + n
    }
    
    // Returning a complex iterator chain without naming its type
    pub fn even_squares(limit: u32) -> impl Iterator<Item = u32> {
        (0..limit).filter(|n| n % 2 == 0).map(|n| n * n)
    }
    

    Type Signatures

    ConceptOCamlRust
    Generic argument'a -> string (inferred)fn f(x: impl Display)
    Returning a functionint -> int (always concrete)impl Fn(i32) -> i32 (opaque)
    Returning an iteratormust name the module typeimpl Iterator<Item = u32>
    Multiple bounds('a : S1) ('a : S2) (modules)impl Trait1 + Trait2

    Key Insights

  • Argument position = sugar for generics. fn f(x: impl Display) and fn f<T: Display>(x: T) are identical after monomorphization; impl Trait simply drops the explicit type-parameter name when you don't need to reference T elsewhere in the signature.
  • Return position creates an opaque type. OCaml always exposes the concrete type in its inferred type signature (e.g., int -> int). Rust's impl Trait hides it: the caller only knows the trait bound, not the underlying struct or closure type. This is an existential type from the caller's view.
  • Zero-cost abstraction. Unlike Box<dyn Trait>, impl Trait is resolved at compile time with no heap allocation and no vtable dispatch. The compiler monomorphizes each call site.
  • Single concrete type required in return position. If different branches return different concrete types, the compiler rejects impl Trait. Use Box<dyn Trait> for runtime polymorphism across branches; impl Trait is a compile-time mechanism only.
  • Iterator ergonomics. The iterator adaptor chain (0..n).filter(...).map(...) produces a type like Map<Filter<Range<u32>, …>, …> — impossible to write by hand. impl Iterator<Item = u32> makes it trivial to return such chains from public APIs.
  • When to Use Each Style

    **Use impl Trait in argument position when:** you have a simple trait bound and don't need to reference the type parameter elsewhere in the signature — keeps the function header clean.

    **Use impl Trait in return position when:* you want to return a closure, a complex iterator chain, or any type that is private / unnamed, and all code paths return the same* concrete type.

    **Use Box<dyn Trait> instead when:** different branches need to return different concrete types, or you need to store the value in a struct field without making the struct generic.

    Exercises

  • Write make_multiplier(n: i32) -> impl Fn(i32) -> i32 and verify it works with apply_twice from the closure examples.
  • Create a function naturals_from(start: u64) -> impl Iterator<Item = u64> returning an infinite lazy sequence.
  • Try returning two different impl Fn types from the same function under an if condition — observe the error, then fix it with Box<dyn Fn>.
  • Open Source Repos