ExamplesBy LevelBy TopicLearning Paths
390 Intermediate

390: Type Alias and `impl Trait`

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "390: Type Alias and `impl Trait`" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Complex iterator chains in Rust have unwritable types: `Filter<Map<IntoIter<i32>, fn(i32) -> i32>, fn(&i32) -> bool>` is the actual return type of a filtered map. Key difference from OCaml: 1. **Inference**: Rust infers the concrete type behind `impl Trait` and enforces it's a single type; OCaml infers the full concrete type at definition and can expose or hide it via `.mli`.

Tutorial

The Problem

Complex iterator chains in Rust have unwritable types: Filter<Map<IntoIter<i32>, fn(i32) -> i32>, fn(&i32) -> bool> is the actual return type of a filtered map. Before impl Trait (stabilized in Rust 1.26), returning such types required boxing with Box<dyn Iterator>, adding heap allocation and vtable overhead. impl Trait in return position lets the compiler infer the concrete type while hiding it from the caller — giving static dispatch performance with ergonomic opaque types. Type aliases make these patterns reusable.

impl Trait return types appear throughout std (.filter(), .map(), .chain()), tokio's async fn desugaring, and any performance-sensitive API that returns complex iterator or future types.

🎯 Learning Outcomes

  • • Understand impl Trait in return position as a way to hide complex concrete types
  • • Learn the difference between impl Trait (static dispatch, one concrete type) and Box<dyn Trait> (dynamic dispatch, any type)
  • • See how type aliases (type BoxedIter<T> = Box<dyn Iterator<Item = T>>) improve readability
  • • Understand when to choose impl Trait vs. Box<dyn Trait> (lifetime, heterogeneous storage, recursion)
  • • Learn how closures captured in impl Iterator prevent naming the return type
  • Code Example

    #![allow(clippy::all)]
    //! Type Alias Impl Trait
    
    pub fn make_counter(start: i32, end: i32) -> impl Iterator<Item = i32> {
        start..end
    }
    pub fn make_even_filter(v: Vec<i32>) -> impl Iterator<Item = i32> {
        v.into_iter().filter(|x| x % 2 == 0)
    }
    pub fn squares(n: u32) -> impl Iterator<Item = i64> {
        (1..=n).map(|x| (x as i64) * (x as i64))
    }
    
    pub type BoxedIter<T> = Box<dyn Iterator<Item = T>>;
    pub fn range_boxed(start: i32, end: i32) -> BoxedIter<i32> {
        Box::new(start..end)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_counter() {
            assert_eq!(make_counter(1, 4).collect::<Vec<_>>(), vec![1, 2, 3]);
        }
        #[test]
        fn test_even_filter() {
            assert_eq!(
                make_even_filter(vec![1, 2, 3, 4]).collect::<Vec<_>>(),
                vec![2, 4]
            );
        }
        #[test]
        fn test_squares() {
            assert_eq!(squares(3).collect::<Vec<_>>(), vec![1, 4, 9]);
        }
        #[test]
        fn test_boxed() {
            assert_eq!(range_boxed(1, 4).collect::<Vec<_>>(), vec![1, 2, 3]);
        }
    }

    Key Differences

  • Inference: Rust infers the concrete type behind impl Trait and enforces it's a single type; OCaml infers the full concrete type at definition and can expose or hide it via .mli.
  • Dynamic vs. static: Rust explicitly chooses impl Trait (static) or Box<dyn Trait> (dynamic); OCaml uses dynamic dispatch for objects, static for modules.
  • Closure types: Rust closures are unique anonymous types requiring impl Trait or Box<dyn Fn()> in return positions; OCaml function types are first-class and returnable as 'a -> 'b.
  • Type aliases: Rust's type BoxedIter<T> = Box<dyn Iterator<Item = T>> creates a generic alias; OCaml's type 'a iter = 'a Seq.t is equivalent and idiomatic.
  • OCaml Approach

    OCaml handles abstract return types through module signatures and functors. A function returning an abstract 'a Seq.t hides the concrete sequence implementation. OCaml's lazy sequences (Seq.t) naturally compose like iterators. OCaml doesn't need impl Trait because function types are already abstract in module interfaces — the .mli file determines what's exposed.

    Full Source

    #![allow(clippy::all)]
    //! Type Alias Impl Trait
    
    pub fn make_counter(start: i32, end: i32) -> impl Iterator<Item = i32> {
        start..end
    }
    pub fn make_even_filter(v: Vec<i32>) -> impl Iterator<Item = i32> {
        v.into_iter().filter(|x| x % 2 == 0)
    }
    pub fn squares(n: u32) -> impl Iterator<Item = i64> {
        (1..=n).map(|x| (x as i64) * (x as i64))
    }
    
    pub type BoxedIter<T> = Box<dyn Iterator<Item = T>>;
    pub fn range_boxed(start: i32, end: i32) -> BoxedIter<i32> {
        Box::new(start..end)
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_counter() {
            assert_eq!(make_counter(1, 4).collect::<Vec<_>>(), vec![1, 2, 3]);
        }
        #[test]
        fn test_even_filter() {
            assert_eq!(
                make_even_filter(vec![1, 2, 3, 4]).collect::<Vec<_>>(),
                vec![2, 4]
            );
        }
        #[test]
        fn test_squares() {
            assert_eq!(squares(3).collect::<Vec<_>>(), vec![1, 4, 9]);
        }
        #[test]
        fn test_boxed() {
            assert_eq!(range_boxed(1, 4).collect::<Vec<_>>(), vec![1, 2, 3]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_counter() {
            assert_eq!(make_counter(1, 4).collect::<Vec<_>>(), vec![1, 2, 3]);
        }
        #[test]
        fn test_even_filter() {
            assert_eq!(
                make_even_filter(vec![1, 2, 3, 4]).collect::<Vec<_>>(),
                vec![2, 4]
            );
        }
        #[test]
        fn test_squares() {
            assert_eq!(squares(3).collect::<Vec<_>>(), vec![1, 4, 9]);
        }
        #[test]
        fn test_boxed() {
            assert_eq!(range_boxed(1, 4).collect::<Vec<_>>(), vec![1, 2, 3]);
        }
    }

    Deep Comparison

    OCaml vs Rust: 390-type-alias-impl-trait

    Exercises

  • Generator pattern: Write a function fibonacci() -> impl Iterator<Item = u64> using std::iter::from_fn with captured mutable state. The iterator should produce Fibonacci numbers indefinitely.
  • Return multiple iterators: Write a function choose_iter(flag: bool) -> Box<dyn Iterator<Item = i32>> that returns either (0..10) or (10..0) based on the flag. Explain why impl Trait cannot be used here but Box<dyn> can.
  • Chaining with impl Trait: Implement a Pipeline<T> builder that accumulates impl Fn(T) -> T transformations and has a run(input: T) -> T method. Use impl Trait bounds to avoid boxing.
  • Open Source Repos