ExamplesBy LevelBy TopicLearning Paths
396 Intermediate

396: Simulating Trait Specialization

Functional Programming

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

  • • Understand why true specialization (overriding blanket impls) is unsound and unstable in Rust
  • • Learn the supertrait layering approach to simulate specialization
  • • See how FastProcess: Process allows specific types to opt into faster code paths
  • • Understand how callers can require either the generic or specialized interface
  • • Learn the performance implications: generic blanket impl vs. type-specific implementation
  • Code 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

  • 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.
  • Call site: Rust specialization simulation requires two separate trait bounds; OCaml achieves type-specific dispatch via modules opened in the right scope.
  • Soundness: Rust's specialization has known soundness issues with lifetime variance; OCaml's module system avoids this category of problem.
  • Default methods: Rust's supertrait approach can use default methods for the generic path and override in specific impls; OCaml modules use 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"));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Serialization specialization: Define 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.
  • Equality specialization: Build 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.
  • Benchmark both paths: Using criterion or a simple timing loop, measure the performance difference between the generic Process path (via format!("{:?}", val)) and the FastProcess path for i32 operations. Quantify the speedup.
  • Open Source Repos