392: `impl Trait` in Argument Position
Tutorial Video
Text description (accessibility)
This video demonstrates the "392: `impl Trait` in Argument Position" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Generic function signatures with multiple trait bounds become syntactically heavy: `fn process<I: Iterator<Item = i32>, F: Fn(i32) -> i32>(items: I, f: F)`. Key difference from OCaml: 1. **Syntax**: Rust requires explicit `impl Trait` or type parameter syntax; OCaml infers constraint from usage automatically.
Tutorial
The Problem
Generic function signatures with multiple trait bounds become syntactically heavy: fn process<I: Iterator<Item = i32>, F: Fn(i32) -> i32>(items: I, f: F). Argument-position impl Trait provides syntactic sugar: fn process(items: impl Iterator<Item = i32>, f: impl Fn(i32) -> i32). Unlike return-position impl Trait, argument-position impl Trait is exactly equivalent to a generic type parameter — each call site can pass a different concrete type, and the function is monomorphized for each one. The syntax is cleaner but the semantics are identical to bounded generics.
This syntactic pattern appears throughout std, tokio, rayon, and modern Rust APIs as a more readable alternative to explicit type parameters.
🎯 Learning Outcomes
impl Trait is syntactic sugar for bounded genericsimpl Iterator<Item = T> accepts any iterator type producing T valuesimpl Fn(i32) -> i32 accepts any callable (closure, function pointer, custom Fn impl)impl Trait (argument position allows multiple types per parameter)impl Trait is clearer than explicit type parametersCode Example
#![allow(clippy::all)]
//! impl Trait in Argument Position
pub fn print_all(items: impl Iterator<Item = impl std::fmt::Display>) {
for item in items {
println!("{}", item);
}
}
pub fn sum_all(nums: impl Iterator<Item = i32>) -> i32 {
nums.sum()
}
pub fn process<F: Fn(i32) -> i32>(items: impl Iterator<Item = i32>, f: F) -> Vec<i32> {
items.map(f).collect()
}
pub fn debug_any(val: impl std::fmt::Debug) -> String {
format!("{:?}", val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum_all(vec![1, 2, 3, 4].into_iter()), 10);
}
#[test]
fn test_process() {
assert_eq!(process(vec![1, 2, 3].into_iter(), |x| x * 2), vec![2, 4, 6]);
}
#[test]
fn test_debug() {
assert!(debug_any(vec![1, 2]).contains("1"));
}
#[test]
fn test_empty() {
assert_eq!(sum_all(std::iter::empty()), 0);
}
}Key Differences
impl Trait or type parameter syntax; OCaml infers constraint from usage automatically.impl Trait parameter position with different concrete types in different calls; OCaml's polymorphism is implicit.OCaml Approach
OCaml's polymorphic functions achieve the same without special syntax. let sum_all nums = Seq.fold_left (+) 0 nums works for any integer sequence. OCaml uses structural typing and type inference — the function type is inferred to be int Seq.t -> int automatically. Higher-order functions accepting callbacks use ('a -> 'b) -> ... types, which are equivalent to Rust's impl Fn(A) -> B.
Full Source
#![allow(clippy::all)]
//! impl Trait in Argument Position
pub fn print_all(items: impl Iterator<Item = impl std::fmt::Display>) {
for item in items {
println!("{}", item);
}
}
pub fn sum_all(nums: impl Iterator<Item = i32>) -> i32 {
nums.sum()
}
pub fn process<F: Fn(i32) -> i32>(items: impl Iterator<Item = i32>, f: F) -> Vec<i32> {
items.map(f).collect()
}
pub fn debug_any(val: impl std::fmt::Debug) -> String {
format!("{:?}", val)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum_all(vec![1, 2, 3, 4].into_iter()), 10);
}
#[test]
fn test_process() {
assert_eq!(process(vec![1, 2, 3].into_iter(), |x| x * 2), vec![2, 4, 6]);
}
#[test]
fn test_debug() {
assert!(debug_any(vec![1, 2]).contains("1"));
}
#[test]
fn test_empty() {
assert_eq!(sum_all(std::iter::empty()), 0);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sum() {
assert_eq!(sum_all(vec![1, 2, 3, 4].into_iter()), 10);
}
#[test]
fn test_process() {
assert_eq!(process(vec![1, 2, 3].into_iter(), |x| x * 2), vec![2, 4, 6]);
}
#[test]
fn test_debug() {
assert!(debug_any(vec![1, 2]).contains("1"));
}
#[test]
fn test_empty() {
assert_eq!(sum_all(std::iter::empty()), 0);
}
}
Deep Comparison
OCaml vs Rust: 392-impl-trait-argument
Exercises
fn collect_sorted<T: Ord>(items: impl Iterator<Item = T>) -> Vec<T> that collects all items and sorts them. Show it works with i32, String, and a custom Ord type.fn pipeline<A, B, C>(items: impl Iterator<Item = A>, f: impl Fn(A) -> B, g: impl Fn(B) -> C) -> Vec<C> that applies two transformations. Test with string length then squaring.sum_all and process using explicit generic type parameters instead of impl Trait. Then try rewriting them using Box<dyn Trait>. Write a comment explaining which form is best for each use case.