873-associated-types — Associated Types
Tutorial
The Problem
When a trait operation involves a type that varies per implementation — like the element type of a container, or the output type of an addition — you need a way to express that relationship without making the trait itself generic. Rust's associated types solve this: a trait Container declares type Item, and each implementation specifies what Item is. This avoids the ambiguity of generic traits (where multiple Container<i32> and Container<String> implementations could coexist on the same struct) and makes trait bounds more readable. OCaml's module types use the same idea with type t and type item declarations inside module signatures.
🎯 Learning Outcomes
Item typestype declarationsIterator::Item and Add::OutputCode Example
trait Container {
type Item;
fn empty() -> Self;
fn push(&mut self, item: Self::Item);
fn pop(&mut self) -> Option<Self::Item>;
}
impl<T> Container for Stack<T> {
type Item = T;
fn empty() -> Self { Stack { items: Vec::new() } }
fn push(&mut self, item: T) { self.items.push(item); }
fn pop(&mut self) -> Option<T> { self.items.pop() }
}Key Differences
where C::Item: Display; OCaml uses with type item = ... or functor application.impl block; OCaml infers them from module structure.Iterator::Item, Add::Output, Index::Output all use associated types — this pattern is pervasive; OCaml uses it in Map.S, Set.S, etc.OCaml Approach
OCaml's module type Container = sig type t; type item; val push: item -> t -> t; ... end directly parallels Rust's associated type design. A stack module Stack: Container with type item = int fixes the item type at instantiation. The with type refinement in OCaml is the equivalent of specifying type Item = i32 in a Rust impl. OCaml module types also support abstract types that become concrete in implementations, just like Rust's associated types.
Full Source
#![allow(clippy::all)]
// Example 079: Associated Types
// OCaml module types → Rust associated types in traits
// === Approach 1: Trait with associated type ===
trait Container {
type Item;
fn empty() -> Self;
fn push(&mut self, item: Self::Item);
fn pop(&mut self) -> Option<Self::Item>;
fn is_empty(&self) -> bool;
fn size(&self) -> usize;
}
struct Stack<T> {
items: Vec<T>,
}
impl<T> Container for Stack<T> {
type Item = T;
fn empty() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn size(&self) -> usize {
self.items.len()
}
}
// === Approach 2: Associated type for output (like Add trait) ===
trait Combinable {
type Output;
fn combine(&self, other: &Self) -> Self::Output;
}
#[derive(Debug, Clone)]
struct Point {
x: f64,
y: f64,
}
impl Combinable for Point {
type Output = f64; // distance between points
fn combine(&self, other: &Self) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
impl Combinable for String {
type Output = String;
fn combine(&self, other: &Self) -> String {
format!("{}{}", self, other)
}
}
// === Approach 3: Multiple associated types ===
trait Transformer {
type Input;
type Output;
fn transform(&self, input: Self::Input) -> Self::Output;
}
struct StringLength;
impl Transformer for StringLength {
type Input = String;
type Output = usize;
fn transform(&self, input: String) -> usize {
input.len()
}
}
struct Doubler;
impl Transformer for Doubler {
type Input = i32;
type Output = i32;
fn transform(&self, input: i32) -> i32 {
input * 2
}
}
// Generic function using associated types
fn apply_transform<T: Transformer>(t: &T, input: T::Input) -> T::Output {
t.transform(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stack_operations() {
let mut s: Stack<i32> = Container::empty();
assert!(s.is_empty());
s.push(10);
s.push(20);
assert_eq!(s.size(), 2);
assert_eq!(s.pop(), Some(20));
assert_eq!(s.pop(), Some(10));
assert_eq!(s.pop(), None);
assert!(s.is_empty());
}
#[test]
fn test_string_stack() {
let mut s: Stack<String> = Container::empty();
s.push("hello".into());
s.push("world".into());
assert_eq!(s.pop(), Some("world".to_string()));
}
#[test]
fn test_point_distance() {
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 3.0, y: 4.0 };
assert!((p1.combine(&p2) - 5.0).abs() < 1e-10);
}
#[test]
fn test_string_combine() {
let s1 = "foo".to_string();
let s2 = "bar".to_string();
assert_eq!(s1.combine(&s2), "foobar");
}
#[test]
fn test_transformers() {
assert_eq!(apply_transform(&StringLength, "test".into()), 4);
assert_eq!(apply_transform(&Doubler, 5), 10);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stack_operations() {
let mut s: Stack<i32> = Container::empty();
assert!(s.is_empty());
s.push(10);
s.push(20);
assert_eq!(s.size(), 2);
assert_eq!(s.pop(), Some(20));
assert_eq!(s.pop(), Some(10));
assert_eq!(s.pop(), None);
assert!(s.is_empty());
}
#[test]
fn test_string_stack() {
let mut s: Stack<String> = Container::empty();
s.push("hello".into());
s.push("world".into());
assert_eq!(s.pop(), Some("world".to_string()));
}
#[test]
fn test_point_distance() {
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 3.0, y: 4.0 };
assert!((p1.combine(&p2) - 5.0).abs() < 1e-10);
}
#[test]
fn test_string_combine() {
let s1 = "foo".to_string();
let s2 = "bar".to_string();
assert_eq!(s1.combine(&s2), "foobar");
}
#[test]
fn test_transformers() {
assert_eq!(apply_transform(&StringLength, "test".into()), 4);
assert_eq!(apply_transform(&Doubler, 5), 10);
}
}
Deep Comparison
Comparison: Associated Types
Container with Associated Type
OCaml:
module type Container = sig
type t
type item
val empty : t
val push : item -> t -> t
val pop : t -> (item * t) option
end
module Stack : Container with type item = int = struct
type item = int
type t = int list
let empty = []
let push x xs = x :: xs
let pop = function [] -> None | x :: xs -> Some (x, xs)
end
Rust:
trait Container {
type Item;
fn empty() -> Self;
fn push(&mut self, item: Self::Item);
fn pop(&mut self) -> Option<Self::Item>;
}
impl<T> Container for Stack<T> {
type Item = T;
fn empty() -> Self { Stack { items: Vec::new() } }
fn push(&mut self, item: T) { self.items.push(item); }
fn pop(&mut self) -> Option<T> { self.items.pop() }
}
Transformer Pattern
OCaml:
module type Transformer = sig
type input
type output
val transform : input -> output
end
module StringLen : Transformer with type input = string and type output = int = struct
type input = string
type output = int
let transform = String.length
end
Rust:
trait Transformer {
type Input;
type Output;
fn transform(&self, input: Self::Input) -> Self::Output;
}
impl Transformer for StringLength {
type Input = String;
type Output = usize;
fn transform(&self, input: String) -> usize { input.len() }
}
Exercises
Parseable trait with type Error as an associated type, and implement it for i32 and f64 with different error types.map method to the Container trait using an associated type MappedOutput<U> (hint: this requires generic associated types in Rust 1.65+).Graph trait with type Node and type Edge associated types, and a simple adjacency-list graph that satisfies it.