382: Associated Types (Advanced)
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
type Item in a trait creates a functional dependency (the implementor determines the type)where Self::Item: Clone associated type bound syntaxCode 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
let x = container.to_vec()); with type parameters, callers often need explicit annotations (let x: String = wrapper.convert()).ConvertTo<String> and ConvertTo<i32>); associated types allow only one impl per trait per type.type t); Rust's type parameters map to functor parameters.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);
}
}#[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 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.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>.trait Serialize<Format> and refactor it to use an associated type. Discuss in a code comment which design is better and why.