396: Simulating Trait Specialization
Tutorial Video
Text description (accessibility)
This video demonstrates the "396: Simulating Trait Specialization" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Trait specialization allows providing a generic fallback implementation and then overriding it with a more efficient implementation for specific types — like providing a generic `Process` for all `Debug` types but a faster `FastProcess` specifically for `i32`. Key difference from OCaml: 1. **Blanket override**: True Rust specialization (overriding a blanket impl for a specific type) is unstable; OCaml modules can always shadow a generic implementation with a specific one.
Tutorial
The Problem
Trait specialization allows providing a generic fallback implementation and then overriding it with a more efficient implementation for specific types — like providing a generic Process for all Debug types but a faster FastProcess specifically for i32. Rust's specialization feature (feature(specialization)) is unstable and has soundness issues, so the production approach is to use subtrait layering: define a FastProcess: Process supertrait hierarchy where specific types implement the more specific trait.
This pattern appears in std::io::Write buffering (byte-at-a-time vs. bulk writes), std::fmt formatting (specialized for numeric types), and performance-critical libraries needing type-specific optimizations.
🎯 Learning Outcomes
FastProcess: Process allows specific types to opt into faster code pathsCode Example
#![allow(clippy::all)]
//! Simulating Trait Specialization
pub trait Process {
fn process(&self) -> String;
}
impl<T: std::fmt::Debug> Process for T {
fn process(&self) -> String {
format!("Debug: {:?}", self)
}
}
pub trait FastProcess: Process {
fn fast_process(&self) -> String;
}
impl FastProcess for i32 {
fn fast_process(&self) -> String {
format!("Fast i32: {}", self)
}
}
impl FastProcess for String {
fn fast_process(&self) -> String {
format!("Fast String: {}", self)
}
}
pub fn process_any<T: Process>(val: &T) -> String {
val.process()
}
pub fn process_fast<T: FastProcess>(val: &T) -> String {
val.fast_process()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generic() {
assert!(process_any(&vec![1, 2, 3]).contains("Debug"));
}
#[test]
fn test_i32_fast() {
assert!(process_fast(&42i32).contains("Fast i32"));
}
#[test]
fn test_string_fast() {
assert!(process_fast(&"hello".to_string()).contains("Fast String"));
}
#[test]
fn test_i32_generic() {
assert!(process_any(&42i32).contains("Debug"));
}
}Key Differences
include with selective override.OCaml Approach
OCaml handles specialization through module functors and type-indexed dispatch. A process function can check the type at runtime using Obj.tag (unsafe) or use the module system to provide type-specific implementations. The core_kernel library uses functor specialization for performance-critical serialization. OCaml's type inference sometimes achieves specialization through monomorphization in native code compilation.
Full Source
#![allow(clippy::all)]
//! Simulating Trait Specialization
pub trait Process {
fn process(&self) -> String;
}
impl<T: std::fmt::Debug> Process for T {
fn process(&self) -> String {
format!("Debug: {:?}", self)
}
}
pub trait FastProcess: Process {
fn fast_process(&self) -> String;
}
impl FastProcess for i32 {
fn fast_process(&self) -> String {
format!("Fast i32: {}", self)
}
}
impl FastProcess for String {
fn fast_process(&self) -> String {
format!("Fast String: {}", self)
}
}
pub fn process_any<T: Process>(val: &T) -> String {
val.process()
}
pub fn process_fast<T: FastProcess>(val: &T) -> String {
val.fast_process()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generic() {
assert!(process_any(&vec![1, 2, 3]).contains("Debug"));
}
#[test]
fn test_i32_fast() {
assert!(process_fast(&42i32).contains("Fast i32"));
}
#[test]
fn test_string_fast() {
assert!(process_fast(&"hello".to_string()).contains("Fast String"));
}
#[test]
fn test_i32_generic() {
assert!(process_any(&42i32).contains("Debug"));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generic() {
assert!(process_any(&vec![1, 2, 3]).contains("Debug"));
}
#[test]
fn test_i32_fast() {
assert!(process_fast(&42i32).contains("Fast i32"));
}
#[test]
fn test_string_fast() {
assert!(process_fast(&"hello".to_string()).contains("Fast String"));
}
#[test]
fn test_i32_generic() {
assert!(process_any(&42i32).contains("Debug"));
}
}
Deep Comparison
OCaml vs Rust: 396-trait-specialization-sim
Exercises
trait Serialize with a generic fn to_bytes(&self) -> Vec<u8> using format!("{:?}"). Then define trait FastSerialize: Serialize for types with known fixed-size binary encoding. Implement for i32, f32, and u64.trait SmartEq with a generic O(n) equality check and trait HashEq: SmartEq that uses a hash for O(1) equality. Show that HashEq types get the fast path while all other types fall back to linear comparison.Process path (via format!("{:?}", val)) and the FastProcess path for i32 operations. Quantify the speedup.