ExamplesBy LevelBy TopicLearning Paths
382 Advanced

382: Associated Types (Advanced)

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "382: Associated Types (Advanced)" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Trait design faces a recurring question: should a related type be an associated type or a type parameter? Key difference from OCaml: 1. **Disambiguation**: With associated types, the compiler can infer the type without annotation (`let x = container.to_vec()`); with type parameters, callers often need explicit annotations (`let x: String = wrapper.convert()`).

Tutorial

The Problem

Trait design faces a recurring question: should a related type be an associated type or a type parameter? Type parameters allow multiple implementations per type (impl ConvertTo<String> for Foo and impl ConvertTo<i32> for Foo). Associated types enforce a single canonical implementation (type Item in Iterator — a type can only be one kind of iterator at a time). Choosing wrong leads to either ambiguous type inference or unnecessarily restricted APIs.

Associated types appear throughout std: Iterator::Item, Add::Output, Deref::Target, Future::Output. The choice between associated types and type parameters is one of the most important API design decisions in Rust.

🎯 Learning Outcomes

  • • Understand when to use associated types vs. type parameters in trait definitions
  • • Learn how associated types enable cleaner type inference at call sites
  • • See how type Item in a trait creates a functional dependency (the implementor determines the type)
  • • Understand how type parameters allow multiple implementations of the same trait on one type
  • • Learn the where Self::Item: Clone associated type bound syntax
  • Code Example

    #![allow(clippy::all)]
    //! Associated Types vs Type Parameters
    //!
    //! When to use each for cleaner APIs.
    
    /// Container trait with associated type
    pub trait Container {
        type Item;
        fn empty() -> Self;
        fn add(&mut self, item: Self::Item);
        fn to_vec(&self) -> Vec<Self::Item>
        where
            Self::Item: Clone;
    }
    
    /// Type parameter trait allows multiple impls
    pub trait ConvertTo<T> {
        fn convert(&self) -> T;
    }
    
    pub struct Stack<T>(Vec<T>);
    
    impl<T: Clone> Container for Stack<T> {
        type Item = T;
        fn empty() -> Self {
            Stack(vec![])
        }
        fn add(&mut self, item: T) {
            self.0.push(item);
        }
        fn to_vec(&self) -> Vec<T> {
            self.0.clone()
        }
    }
    
    pub struct Wrapper(pub i32);
    
    impl ConvertTo<String> for Wrapper {
        fn convert(&self) -> String {
            self.0.to_string()
        }
    }
    
    impl ConvertTo<f64> for Wrapper {
        fn convert(&self) -> f64 {
            self.0 as f64
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_container() {
            let mut s = Stack::<i32>::empty();
            s.add(1);
            s.add(2);
            s.add(3);
            assert_eq!(s.to_vec(), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_convert_to_string() {
            let w = Wrapper(42);
            let s: String = w.convert();
            assert_eq!(s, "42");
        }
    
        #[test]
        fn test_convert_to_f64() {
            let w = Wrapper(42);
            let f: f64 = w.convert();
            assert_eq!(f, 42.0);
        }
    
        #[test]
        fn test_multiple_impls() {
            let w = Wrapper(10);
            assert_eq!(ConvertTo::<String>::convert(&w), "10");
            assert_eq!(ConvertTo::<f64>::convert(&w), 10.0);
        }
    }

    Key Differences

  • Disambiguation: With associated types, the compiler can infer the type without annotation (let x = container.to_vec()); with type parameters, callers often need explicit annotations (let x: String = wrapper.convert()).
  • Multiple impls: Rust allows multiple type-parameter impls on one type (ConvertTo<String> and ConvertTo<i32>); associated types allow only one impl per trait per type.
  • OCaml correspondence: Rust's associated types map to OCaml's abstract types in signatures (type t); Rust's type parameters map to functor parameters.
  • Bounds syntax: Rust uses where Self::Item: Clone for associated type bounds; OCaml uses constraints in module signatures (module type S = sig type t constraint t = ...).
  • OCaml Approach

    OCaml's module system handles this distinction through module signatures. An associated type maps to a type alias in a module signature: module type Container = sig type item ... end. Multiple conversions map to different modules or functor parameters. OCaml's type inference handles associated types naturally since modules carry their type definitions with them.

    Full Source

    #![allow(clippy::all)]
    //! Associated Types vs Type Parameters
    //!
    //! When to use each for cleaner APIs.
    
    /// Container trait with associated type
    pub trait Container {
        type Item;
        fn empty() -> Self;
        fn add(&mut self, item: Self::Item);
        fn to_vec(&self) -> Vec<Self::Item>
        where
            Self::Item: Clone;
    }
    
    /// Type parameter trait allows multiple impls
    pub trait ConvertTo<T> {
        fn convert(&self) -> T;
    }
    
    pub struct Stack<T>(Vec<T>);
    
    impl<T: Clone> Container for Stack<T> {
        type Item = T;
        fn empty() -> Self {
            Stack(vec![])
        }
        fn add(&mut self, item: T) {
            self.0.push(item);
        }
        fn to_vec(&self) -> Vec<T> {
            self.0.clone()
        }
    }
    
    pub struct Wrapper(pub i32);
    
    impl ConvertTo<String> for Wrapper {
        fn convert(&self) -> String {
            self.0.to_string()
        }
    }
    
    impl ConvertTo<f64> for Wrapper {
        fn convert(&self) -> f64 {
            self.0 as f64
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_container() {
            let mut s = Stack::<i32>::empty();
            s.add(1);
            s.add(2);
            s.add(3);
            assert_eq!(s.to_vec(), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_convert_to_string() {
            let w = Wrapper(42);
            let s: String = w.convert();
            assert_eq!(s, "42");
        }
    
        #[test]
        fn test_convert_to_f64() {
            let w = Wrapper(42);
            let f: f64 = w.convert();
            assert_eq!(f, 42.0);
        }
    
        #[test]
        fn test_multiple_impls() {
            let w = Wrapper(10);
            assert_eq!(ConvertTo::<String>::convert(&w), "10");
            assert_eq!(ConvertTo::<f64>::convert(&w), 10.0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_container() {
            let mut s = Stack::<i32>::empty();
            s.add(1);
            s.add(2);
            s.add(3);
            assert_eq!(s.to_vec(), vec![1, 2, 3]);
        }
    
        #[test]
        fn test_convert_to_string() {
            let w = Wrapper(42);
            let s: String = w.convert();
            assert_eq!(s, "42");
        }
    
        #[test]
        fn test_convert_to_f64() {
            let w = Wrapper(42);
            let f: f64 = w.convert();
            assert_eq!(f, 42.0);
        }
    
        #[test]
        fn test_multiple_impls() {
            let w = Wrapper(10);
            assert_eq!(ConvertTo::<String>::convert(&w), "10");
            assert_eq!(ConvertTo::<f64>::convert(&w), 10.0);
        }
    }

    Deep Comparison

    OCaml vs Rust: Associated Types

    Exercises

  • Graph trait with associated types: Design a Graph trait with type Vertex and type Edge as associated types. Implement it for both an adjacency list graph and a matrix graph, demonstrating that each has a unique vertex/edge type.
  • Converter with associated input: Create a Parser trait with type Output as an associated type. Implement it for parsing &str into i32, f64, and a custom Color type, then write a generic function parse_all<P: Parser>(inputs: &[&str]) -> Vec<P::Output>.
  • Refactor to reduce ambiguity: Take a trait using a type parameter trait Serialize<Format> and refactor it to use an associated type. Discuss in a code comment which design is better and why.
  • Open Source Repos