impl Trait in Function Signatures
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
impl Trait in argument position (sugar for generics) from return position (opaque type)impl Iterator<Item = T> enables lazy, composable pipelinesBox<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
impl Trait hides them at the individual function level.impl Fn in return position avoids heap allocation; OCaml closures are always heap-allocated.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);
}
}#[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
| Concept | OCaml | Rust |
|---|---|---|
| Generic argument | 'a -> string (inferred) | fn f(x: impl Display) |
| Returning a function | int -> int (always concrete) | impl Fn(i32) -> i32 (opaque) |
| Returning an iterator | must name the module type | impl Iterator<Item = u32> |
| Multiple bounds | ('a : S1) ('a : S2) (modules) | impl Trait1 + Trait2 |
Key Insights
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.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.Box<dyn Trait>, impl Trait is resolved at compile time with no heap allocation and no vtable dispatch. The compiler monomorphizes each call site.impl Trait. Use Box<dyn Trait> for runtime polymorphism across branches; impl Trait is a compile-time mechanism only.(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
make_multiplier(n: i32) -> impl Fn(i32) -> i32 and verify it works with apply_twice from the closure examples.naturals_from(start: u64) -> impl Iterator<Item = u64> returning an infinite lazy sequence.impl Fn types from the same function under an if condition — observe the error, then fix it with Box<dyn Fn>.