ExamplesBy LevelBy TopicLearning Paths
391 Intermediate

391: `impl Trait` in Return Position

Functional Programming

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

  • • Understand return-position impl Trait as a way to return unnameable types with static dispatch
  • • Learn how closures captured with move can be returned as impl Fn
  • • See how std::iter::from_fn creates stateful iterators without defining new types
  • • Understand lifetime annotations with impl Trait + '_ for borrowed return types
  • • Learn the constraint: a function can only return ONE concrete type behind impl Trait
  • Code 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

  • Necessity: OCaml doesn't need impl Trait for closures since functions are first-class and polymorphically typed; Rust needs it because closures have unique anonymous types.
  • Single-type constraint: Rust's impl Trait return must be one concrete type; OCaml functions can return different types in different branches (via union types or polymorphism).
  • Async desugaring: Rust's async fn implicitly uses RPIT (-> impl Future); OCaml's Lwt or Eio use explicit promise types.
  • Performance: Both achieve zero allocation for closure returns; Rust makes this explicit with 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]);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Memoized adder factory: Write 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.
  • Iterator pipeline: Write pipeline(data: Vec<i32>) -> impl Iterator<Item = String> that filters evens, squares them, and converts to strings — all lazily without collecting intermediate results.
  • Stateful counter: Implement 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.
  • Open Source Repos