390: Type Alias and `impl Trait`
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
impl Trait in return position as a way to hide complex concrete typesimpl Trait (static dispatch, one concrete type) and Box<dyn Trait> (dynamic dispatch, any type)type BoxedIter<T> = Box<dyn Iterator<Item = T>>) improve readabilityimpl Trait vs. Box<dyn Trait> (lifetime, heterogeneous storage, recursion)impl Iterator prevent naming the return typeCode 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
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.impl Trait (static) or Box<dyn Trait> (dynamic); OCaml uses dynamic dispatch for objects, static for modules.impl Trait or Box<dyn Fn()> in return positions; OCaml function types are first-class and returnable as 'a -> 'b.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]);
}
}#[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
fibonacci() -> impl Iterator<Item = u64> using std::iter::from_fn with captured mutable state. The iterator should produce Fibonacci numbers indefinitely.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.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.