๐Ÿฆ€ Functional Rust
๐ŸŽฌ Traits & Generics Shared behaviour, static vs dynamic dispatch, zero-cost polymorphism.
๐Ÿ“ Text version (for readers / accessibility)

โ€ข Traits define shared behaviour โ€” like interfaces but with default implementations

โ€ข Generics with trait bounds: fn process(item: T) โ€” monomorphized at compile time

โ€ข Static dispatch (impl Trait) = zero cost; dynamic dispatch (dyn Trait) = runtime flexibility via vtable

โ€ข Blanket implementations apply traits to all types matching a bound

โ€ข Associated types and supertraits enable complex type relationships

396: Simulating Trait Specialization

Difficulty: 4 Level: Expert Rust's specialization is unstable โ€” but you can simulate it at compile time using wrapper types and trait dispatch.

The Problem This Solves

Specialization lets you provide a generic implementation of a trait for all types, then override it with a more efficient (or different) implementation for specific types. C++ has it. Haskell has it via overlapping instances. Rust's RFC 1210 proposed it, but it's been stuck in nightly for years due to soundness concerns around lifetimes. In practice, the need arises constantly: a `Serialize` trait that uses a fast numeric path for integers but a debug-format fallback for everything else. A `Display` trait that treats `Vec<u8>` as bytes but everything else as debug output. Without specialization, you're forced to either use `Any`/downcasting (dynamic, runtime cost) or write separate concrete impls with no shared interface. The wrapper-type simulation achieves compile-time dispatch by routing types through different newtype wrappers โ€” each wrapper gets its own trait impl, and the selection happens at the call site. It's more explicit than true specialization, but it's stable, zero-cost, and clearly expresses intent.

The Intuition

Instead of "implement the trait differently for different types," you implement "a different wrapper type" for each behavior. `Default(x)` gets the generic fallback. `Specialized(x)` gets the specific fast path. The compiler resolves which impl to call at compile time based on the wrapper, with no runtime overhead. This is the "newtype dispatch" pattern โ€” you move the specialization decision from the impl resolver to the call site.

How It Works in Rust

struct Generic<T>(T);
struct Fast<T>(T);

trait Serialize {
 fn serialize(&self) -> String;
}

// Blanket fallback for any Debug type
impl<T: std::fmt::Debug> Serialize for Generic<T> {
 fn serialize(&self) -> String { format!("{:?}", self.0) }
}

// Specific fast path for i32
impl Serialize for Fast<i32> {
 fn serialize(&self) -> String { self.0.to_string() }
}

// At the call site, choose explicitly:
println!("{}", Fast(42i32).serialize());          // "42"
println!("{}", Generic(vec![1, 2, 3]).serialize()); // "[1, 2, 3]"
For more automatic dispatch, a marker trait or macro can select the wrapper, but the mechanism is always the same: move the decision to compile time via types, not runtime via `match`.

What This Unlocks

Key Differences

ConceptOCamlRust
Default behaviorFunctor with default implementationsBlanket `impl<T: Bound> Trait for Wrapper<T>`
OverrideModule inclusion + shadowConcrete `impl Trait for Wrapper<ConcreteType>`
SelectionFunctor applicationWrapper type at call site (or macro)
Compile-timeYes (monomorphic modules)Yes (monomorphization)
True specializationN/A in OCamlUnstable (RFC 1210, nightly only)
// Simulating specialization in Rust
// Real specialization is unstable; we simulate with wrapper types + traits

// The "default" behavior via a wrapper
struct Default<T>(T);
struct Specialized<T>(T);

trait Serialize {
    fn serialize(&self) -> String;
}

// Blanket impl for Default wrapper (generic fallback)
impl<T: std::fmt::Debug> Serialize for Default<T> {
    fn serialize(&self) -> String {
        format!("{{"debug": "{:?}"}}", self.0)
    }
}

// "Specialized" impl for numbers
impl Serialize for Specialized<i32> {
    fn serialize(&self) -> String {
        self.0.to_string()  // Just the number, no quotes
    }
}

impl Serialize for Specialized<f64> {
    fn serialize(&self) -> String {
        format!("{:.6}", self.0)
    }
}

impl Serialize for Specialized<bool> {
    fn serialize(&self) -> String {
        if self.0 { "true".to_string() } else { "false".to_string() }
    }
}

impl Serialize for Specialized<String> {
    fn serialize(&self) -> String {
        format!(""{}"", self.0.replace('"', "\""))
    }
}

// Macro to select specialized vs default
macro_rules! serialize_val {
    ($val:expr, specialized) => { Specialized($val).serialize() };
    ($val:expr, default) => { Default($val).serialize() };
    ($val:expr) => { Default($val).serialize() };
}

fn main() {
    println!("i32 (specialized): {}", Specialized(42i32).serialize());
    println!("f64 (specialized): {}", Specialized(3.14f64).serialize());
    println!("bool (specialized): {}", Specialized(true).serialize());
    println!("String (specialized): {}", Specialized("hello".to_string()).serialize());

    #[derive(Debug)] struct Point { x: i32, y: i32 }
    println!("Point (default): {}", Default(Point { x: 1, y: 2 }).serialize());

    println!("\nVia macro:");
    println!("{}", serialize_val!(99i32, specialized));
    println!("{}", serialize_val!(vec![1,2,3]));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_specialized_int() {
        assert_eq!(Specialized(7i32).serialize(), "7");
    }

    #[test]
    fn test_specialized_bool() {
        assert_eq!(Specialized(false).serialize(), "false");
    }

    #[test]
    fn test_default_fallback() {
        #[derive(Debug)] struct Foo(i32);
        let s = Default(Foo(1)).serialize();
        assert!(s.contains("debug"));
    }
}
(* Simulating specialization in OCaml via functors *)

(* Default implementation *)
module type Serializer = sig
  type t
  val serialize : t -> string
end

module DefaultSerializer (T : sig type t val to_string : t -> string end)
  : Serializer with type t = T.t = struct
  type t = T.t
  let serialize x = "{"value":"" ^ T.to_string x ^ ""}"
end

(* "Specialized" for int โ€” more efficient *)
module IntSerializer : Serializer with type t = int = struct
  type t = int
  let serialize n = string_of_int n  (* No quotes for numbers *)
end

module BoolSerializer : Serializer with type t = bool = struct
  type t = bool
  let serialize b = if b then "true" else "false"
end

module StringSerializer = DefaultSerializer(struct
  type t = string
  let to_string x = x
end)

let () =
  Printf.printf "int: %s\n" (IntSerializer.serialize 42);
  Printf.printf "bool: %s\n" (BoolSerializer.serialize true);
  Printf.printf "string: %s\n" (StringSerializer.serialize "hello")