391: `impl Trait` in Return Position
Tutorial Video
Text description (accessibility)
This video demonstrates the "391: `impl Trait` in Return Position" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Closures and complex iterators have unnameable types in Rust. Key difference from OCaml: 1. **Necessity**: OCaml doesn't need `impl Trait` for closures since functions are first
Tutorial
The Problem
Closures and complex iterators have unnameable types in Rust. A closure move |x| x + n has a unique anonymous type that cannot be written in a function signature. Before impl Trait (Rust 1.26), returning closures required Box<dyn Fn> with heap allocation. Return-position impl Trait (RPIT) tells the compiler "return some concrete type implementing this trait" — the caller doesn't know the exact type, but gets static dispatch with no heap allocation. This is essential for returning closures, lazy iterators, and async futures.
RPIT appears in std::iter adapters, async fn desugaring (which returns impl Future), Rust's generator proposal, and any API that wants to hide implementation types while avoiding boxing costs.
🎯 Learning Outcomes
impl Trait as a way to return unnameable types with static dispatchmove can be returned as impl Fnstd::iter::from_fn creates stateful iterators without defining new typesimpl Trait + '_ for borrowed return typesimpl TraitCode Example
#![allow(clippy::all)]
//! impl Trait in Return Position
pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
pub fn make_counter() -> impl Iterator<Item = i32> {
0..
}
pub fn make_greeting(name: &str) -> impl std::fmt::Display + '_ {
format!("Hello, {}!", name)
}
pub fn fibonacci() -> impl Iterator<Item = u64> {
let mut a = 0u64;
let mut b = 1u64;
std::iter::from_fn(move || {
let c = a;
a = b;
b = c + b;
Some(c)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adder() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
}
#[test]
fn test_counter() {
let first5: Vec<_> = make_counter().take(5).collect();
assert_eq!(first5, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_greeting() {
assert_eq!(format!("{}", make_greeting("World")), "Hello, World!");
}
#[test]
fn test_fib() {
let fibs: Vec<_> = fibonacci().take(7).collect();
assert_eq!(fibs, vec![0, 1, 1, 2, 3, 5, 8]);
}
}Key Differences
impl Trait for closures since functions are first-class and polymorphically typed; Rust needs it because closures have unique anonymous types.impl Trait return must be one concrete type; OCaml functions can return different types in different branches (via union types or polymorphism).async fn implicitly uses RPIT (-> impl Future); OCaml's Lwt or Eio use explicit promise types.impl Fn vs. Box<dyn Fn>, OCaml always uses the same value representation.OCaml Approach
OCaml functions can return any value including closures without special syntax — closures are first-class values. let make_adder n = fun x -> x + n naturally returns a function. Iterators are typically 'a Seq.t values with lazy evaluation. OCaml's type inference automatically determines the concrete return type without annotation, similar to what Rust achieves with impl Trait.
Full Source
#![allow(clippy::all)]
//! impl Trait in Return Position
pub fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
pub fn make_counter() -> impl Iterator<Item = i32> {
0..
}
pub fn make_greeting(name: &str) -> impl std::fmt::Display + '_ {
format!("Hello, {}!", name)
}
pub fn fibonacci() -> impl Iterator<Item = u64> {
let mut a = 0u64;
let mut b = 1u64;
std::iter::from_fn(move || {
let c = a;
a = b;
b = c + b;
Some(c)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adder() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
}
#[test]
fn test_counter() {
let first5: Vec<_> = make_counter().take(5).collect();
assert_eq!(first5, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_greeting() {
assert_eq!(format!("{}", make_greeting("World")), "Hello, World!");
}
#[test]
fn test_fib() {
let fibs: Vec<_> = fibonacci().take(7).collect();
assert_eq!(fibs, vec![0, 1, 1, 2, 3, 5, 8]);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adder() {
let add5 = make_adder(5);
assert_eq!(add5(10), 15);
}
#[test]
fn test_counter() {
let first5: Vec<_> = make_counter().take(5).collect();
assert_eq!(first5, vec![0, 1, 2, 3, 4]);
}
#[test]
fn test_greeting() {
assert_eq!(format!("{}", make_greeting("World")), "Hello, World!");
}
#[test]
fn test_fib() {
let fibs: Vec<_> = fibonacci().take(7).collect();
assert_eq!(fibs, vec![0, 1, 1, 2, 3, 5, 8]);
}
}
Deep Comparison
OCaml vs Rust: 391-impl-trait-return
Exercises
make_memoized_adder(n: i32) -> impl FnMut(i32) -> i32 that caches the most recent result using captured mutable state, returning the cached value when called with the same input.pipeline(data: Vec<i32>) -> impl Iterator<Item = String> that filters evens, squares them, and converts to strings — all lazily without collecting intermediate results.make_counter(step: i32) -> impl FnMut() -> i32 returning a closure that increments by step each call, starting at 0. Write tests verifying the state is independent between different closures.