rust-job-prep / phase-01 / day-03.rs
PHASE 01  |  DAY 03 / 15
// rust job ready series — phase 01: core rust

Traits &
Generics

$ rustc day03.rs  →  zero_cost_abstractions_at_compile_time
Day 03 impl Trait dyn Trait where clauses generics trait bounds monomorphization

A trait defines a set of methods that a type must implement — it's Rust's version of an interface or abstract class. Traits are how Rust achieves polymorphism: the same function can work on any type that satisfies a set of trait requirements, without runtime overhead.

Generics let you write code that works for any type that meets specific constraints (trait bounds). The compiler monomorphizes generic code — it generates a separate, concrete version for every type you actually use — so the runtime binary contains zero abstraction overhead. Think of it as "copy-paste by the compiler, paid once at compile time."

The two ways to use traits in function signatures: impl Trait (static dispatch — monomorphized, zero cost, return type must be one concrete type) vs dyn Trait (dynamic dispatch — vtable at runtime, allows heterogeneous collections, small runtime cost). Knowing when to pick which is a key mid-level skill.

CONCEPT 01

Trait Definition & impl

A trait declares what a type must be able to do. You implement it with impl MyTrait for MyType. Multiple types can implement the same trait — that's polymorphism.

CONCEPT 02

impl Trait — Static Dispatch

Used in function params (fn foo(x: impl Trait)) or return position (fn foo() -> impl Trait). The compiler resolves the exact type at compile time — zero runtime cost. Return type must be a single concrete type.

CONCEPT 03

dyn Trait — Dynamic Dispatch

Pointer to a vtable at runtime (Box<dyn Trait>, &dyn Trait). Allows heterogeneous collections — different concrete types behind the same pointer. Small overhead: one extra pointer indirection per call.

CONCEPT 04

where Clauses

Move trait bounds out of the angle brackets into a separate where block for readability. Necessary when bounds get complex or reference associated types: where T: Clone + Debug, T::Item: Send.

CONCEPT 05

Default Method Implementations

Traits can provide default implementations for methods — implementors can override them or use the default. This is how Rust avoids abstract base classes and gives you free behaviour for new types.

CONCEPT 06

Trait Objects Must Be Object-Safe

Not all traits can become dyn Trait. A trait is object-safe if its methods don't use Self by value or have generic type parameters — because the vtable must have a fixed layout for any concrete type.

USE CASE The most basic building block: defining a trait and implementing it on multiple types. This is how Rust achieves polymorphism — any function that accepts impl Greet works with all types that implement it.
day03_define_trait.rs
TRAIT BASICS
// Define a trait — like an interface in other languages
trait Greet {
    fn name(&self) -> &str;

    // Default implementation — free for any implementor
    fn hello(&self) {
        println!("Hello, my name is {}!", self.name());
    }
}

struct Human { name: String }
struct Robot  { model: String }

impl Greet for Human {
    fn name(&self) -> &str { &self.name }
    // `hello` — uses the default implementation above
}

impl Greet for Robot {
    fn name(&self) -> &str { &self.model }
    // Override default for robots
    fn hello(&self) {
        println!("BEEP. Designation: {}. BOOP.", self.name());
    }
}

fn main() {
    let h = Human { name: String::from("Mishal") };
    let r = Robot  { model: String::from("R2-D2")  };

    h.hello(); // "Hello, my name is Mishal!"  (default)
    r.hello(); // "BEEP. Designation: R2-D2. BOOP."  (overridden)
}
UNDER THE HOOD
  • The trait definition generates a vtable shape — a list of function signatures that any implementor must fill.
  • Default method implementations are just Rust code inside the trait body — the compiler pastes them into the implementing type if it doesn't override them. There's no virtual dispatch happening here at the trait level; default methods are resolved statically if called on a concrete type.
  • impl Greet for Human — Rust checks at compile time that every required method (those with no default) is implemented. Missing one = compile error.
  • At this stage, no vtable is used — calling h.hello() on a concrete Human is a direct function call, fully inlined by the optimizer if it wants to.
USE CASE Generics + trait bounds — write one function that works for any type implementing Greet. At compile time the compiler stamps out a separate version for each concrete type used — this is monomorphization.
day03_generic_function.rs
GENERICS
// Generic function — T must implement Greet
fn introduce<T: Greet>(entity: &T) {
    println!("Introducing: {}", entity.name());
    entity.hello();
}

// Multiple trait bounds with + syntax
use std::fmt::Debug;

fn introduce_and_debug<T: Greet + Debug>(entity: &T) {
    entity.hello();
    println!("Debug view: {:?}", entity);
}

// Same thing using WHERE CLAUSE — easier to read with many bounds
fn introduce_where<T>(entity: &T)
where
    T: Greet + Debug,
{
    entity.hello();
    println!("Debug: {:?}", entity);
}

#[derive(Debug)]
struct Human { name: String }
impl Greet for Human {
    fn name(&self) -> &str { &self.name }
}

fn main() {
    let h = Human { name: String::from("Mishal") };
    introduce(&h);
    introduce_where(&h);
}
UNDER THE HOOD
  • Monomorphization: introduce::<Human> and introduce::<Robot> are compiled into two separate functions in the binary. Each call site knows the exact type at compile time — no indirection, no vtable lookup.
  • This is why generics are called "zero-cost abstractions" — the abstraction only exists in source code; the compiled output is as efficient as if you had written separate functions by hand.
  • Trade-off: monomorphization can increase binary size (code bloat) if you use many different concrete types. This is why dyn Trait exists — one function body, multiple types.
  • T: Greet + Debug is a compound trait bound: T must satisfy both traits simultaneously. where is purely ergonomic — same semantics, just more readable when bounds are long.
USE CASE impl Trait in both parameter position (syntactic sugar for a generic bound) and return position (tell the caller "you'll get something that implements this trait" without naming the concrete type — essential for returning closures and iterators).
day03_impl_trait.rs
impl Trait
use std::fmt::Display;

// PARAMETER position — same as fn foo<T: Display>(val: T)
// Difference: with impl Trait here, each param gets its OWN anonymous type.
// fn foo(a: impl Display, b: impl Display) could be two DIFFERENT types.
fn print_value(val: impl Display) {
    println!("Value: {}", val);
}

// RETURN position — "I return something that impl Display, but won't tell you what type"
// Compiler knows the concrete type and optimises it — static dispatch
fn make_greeting(name: &str) -> impl Display {
    format!("Hello, {}!", name) // returns String, but caller sees `impl Display`
}

// KEY USE CASE: returning closures — closures have unique unnameable types
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y  // the closure captures x — its type is unnameable
}

// KEY USE CASE: returning iterators — iterator types are deeply nested
fn evens_up_to(n: u32) -> impl Iterator<Item = u32> {
    (0..=n).filter(|x| x % 2 == 0)
    // actual type: Filter<RangeInclusive<u32>, [closure]> — you don't want to type this
}

fn main() {
    print_value(42);
    print_value("hello");
    println!("{}", make_greeting("Mishal"));

    let add5 = make_adder(5);
    println!("5 + 3 = {}", add5(3));

    let evens: Vec<_> = evens_up_to(10).collect();
    println!("{:?}", evens); // [0, 2, 4, 6, 8, 10]
}
UNDER THE HOOD
  • Parameter position: fn foo(a: impl Display, b: impl Display)a and b can be different concrete types. In contrast, fn foo<T: Display>(a: T, b: T) requires both to be the same type.
  • Return position: the compiler knows the concrete type at compile time and monomorphizes the call — zero overhead. The caller just can't name the type (useful for closures and complex iterator chains).
  • Closures have unique anonymous types (like [closure@src/main.rs:20:5]) that you cannot write in source code — impl Fn(...) is the only way to return them from a regular function.
  • If you need to return different concrete types depending on a runtime condition, impl Trait doesn't work — that's when you need Box<dyn Trait>.
USE CASE dyn Trait enables runtime polymorphism — store different concrete types behind the same pointer, decided at runtime. The classic use case: a heterogeneous Vec<Box<dyn Trait>> holding mixed types.
day03_dyn_trait.rs
dyn Trait
trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> &str;
}

struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }

impl Shape for Circle {
    fn area(&self)   -> f64  { std::f64::consts::PI * self.radius * self.radius }
    fn name(&self)   -> &str  { "Circle" }
}
impl Shape for Rectangle {
    fn area(&self)   -> f64  { self.width * self.height }
    fn name(&self)   -> &str  { "Rectangle" }
}

// `shapes` holds DIFFERENT types — impossible with generics alone
// Box<dyn Shape> is a fat pointer: (data ptr, vtable ptr)
fn print_shapes(shapes: &[Box<dyn Shape>]) {
    for shape in shapes {
        println!("{}: area = {:.2}", shape.name(), shape.area());
    }
}

fn main() {
    let shapes: Vec<Box<dyn Shape>> = vec![
        Box::new(Circle    { radius: 3.0             }),
        Box::new(Rectangle { width: 4.0, height: 5.0 }),
        Box::new(Circle    { radius: 1.5             }),
    ];

    print_shapes(&shapes);
    // Circle:    area = 28.27
    // Rectangle: area = 20.00
    // Circle:    area = 7.07
}
UNDER THE HOOD
  • Box<dyn Shape> is a fat pointer — two machine words: one pointing to the heap-allocated data, one pointing to a vtable (a table of function pointers for that concrete type's trait implementations).
  • When you call shape.area(), Rust looks up the function pointer in the vtable at runtime — one extra pointer indirection per call. This is the "dynamic" in dynamic dispatch.
  • The vtable is generated once per (type, trait) combination and stored in the binary's read-only data — not per instance. Each Box<dyn Shape> carries a pointer to the vtable, not a copy of it.
  • When to pick dyn over impl: when you need heterogeneous types in the same collection, when the concrete type is unknown until runtime (e.g. plugin systems), or when you want a single function body instead of monomorphized copies for every type.
// my own examples — seeing it clearly
USE CASE — MY EXAMPLE The clearest side-by-side comparison: same task done with impl Trait (static) and dyn Trait (dynamic). Shows exactly what each compiles to and when one breaks.
day03_my_impl_vs_dyn.rs
impl vs dyn
trait Speak {
    fn speak(&self) -> String;
}

struct Dog;
struct Cat;
impl Speak for Dog { fn speak(&self) -> String { "Woof!".into() } }
impl Speak for Cat { fn speak(&self) -> String { "Meow!".into() } }

// --- impl Trait (static dispatch) ---
// Compiler generates: fn static_speak_Dog, fn static_speak_Cat
// Zero runtime overhead. ONE concrete type per call.
fn static_speak(animal: &impl Speak) {
    println!("[static]  {}", animal.speak());
}

// --- dyn Trait (dynamic dispatch) ---
// ONE function body, handles ANY Speak type at runtime via vtable.
// Slight overhead per call — but allows heterogeneous collections.
fn dynamic_speak(animal: &dyn Speak) {
    println!("[dynamic] {}", animal.speak());
}

fn main() {
    let dog = Dog;
    let cat = Cat;

    // static: fast, type known at compile time
    static_speak(&dog); // [static]  Woof!
    static_speak(&cat); // [static]  Meow!

    // dynamic: heterogeneous Vec possible
    let animals: Vec<&dyn Speak> = vec![&dog, &cat];
    for a in &animals {
        dynamic_speak(*a);
    }
    // [dynamic] Woof!
    // [dynamic] Meow!

    // ❌ This would NOT compile with impl Trait in return position
    //    if we want different types based on runtime condition:
    // fn pick_animal(is_dog: bool) -> impl Speak {
    //     if is_dog { Dog } else { Cat }  // ❌ ERROR: mismatched types
    // }

    // ✅ Use Box<dyn Speak> instead:
    fn pick_animal(is_dog: bool) -> Box<dyn Speak> {
        if is_dog { Box::new(Dog) } else { Box::new(Cat) }
    }
    println!("{}", pick_animal(true).speak()); // Woof!
}
UNDER THE HOOD
  • static_speak(&dog) resolves to a call to Dog::speak at compile time — the optimizer can even inline it completely.
  • dynamic_speak(&dog) at the call site: (1) the reference is widened into a fat pointer (&Dog data, &Dog's Speak vtable); (2) inside the function, animal.speak() does a vtable lookup: read function pointer from vtable[0], call it with the data pointer as &self.
  • The pick_animal example demonstrates the key rule: impl Trait in return position can only be ONE concrete type — the compiler must know it statically. If the branch returns different types (Dog vs Cat), you must use Box<dyn Trait>.
USE CASE — MY EXAMPLE A realistic where clause — a generic struct (Wrapper<T>) that pretty-prints and compares its content. Without where, the angle brackets would become unreadably long.
day03_my_where_clause.rs
WHERE CLAUSE
use std::fmt::{Debug, Display};

struct Wrapper<T> {
    value: T,
    label: String,
}

// WITHOUT where — messy with many bounds:
// impl<T: Display + Debug + PartialOrd + Clone> Wrapper<T> { ... }

// WITH where — clean, easy to read
impl<T> Wrapper<T>
where
    T: Display + Debug + PartialOrd + Clone,
{
    fn new(value: T, label: &str) -> Self {
        Self { value, label: label.to_string() }
    }

    fn display(&self) {
        println!("[{}] Display: {}  Debug: {:?}", self.label, self.value, self.value);
    }

    fn is_greater_than(&self, other: &T) -> bool {
        self.value > *other
    }
}

fn main() {
    let w1 = Wrapper::new(42_i32, "score");
    let w2 = Wrapper::new("hello", "greeting");

    w1.display(); // [score]    Display: 42    Debug: 42
    w2.display(); // [greeting] Display: hello  Debug: "hello"

    println!("42 > 10? {}", w1.is_greater_than(&10)); // true
}
UNDER THE HOOD
  • The where clause is purely syntactic — it compiles to identical code as putting bounds in the angle brackets. The only difference is human readability.
  • Wrapper<T> is generic over T — the struct itself has no bound. Bounds are placed on the impl block, meaning only the methods that need them require T to be Display + Debug + PartialOrd + Clone. You could have another impl<T> Wrapper<T> block with fewer bounds for other methods.
  • This is a common production pattern: keep the struct itself unconstrained, add bounds only to the specific impl blocks that need them. It maximizes the range of types that can use your struct.
USE CASE — MY EXAMPLE The most important standard-library traits you'll implement in production code: Display, Debug, From/Into, Clone, Default. These unlock operator overloads, format strings, ergonomic conversions, and framework integrations.
day03_my_stdlib_traits.rs
STDLIB TRAITS
use std::fmt; #[derive(Debug, Clone, Default)] // Debug, Clone, Default — auto-derived struct Point { x: f64, y: f64, } // Display — what the user sees. Needed for println!("{}", point) impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } // From — enables ergonomic conversions and the .into() pattern impl From<(f64, f64)> for Point { fn from((x, y): (f64, f64)) -> Self { Self { x, y } } } // PartialEq — enables == operator impl PartialEq for Point { fn eq(&self, other: &Self) -> bool { self.x == other.x && self.y == other.y } } fn main() { let p1 = Point { x: 1.0, y: 2.0 }; let p2: Point = (1.0_f64, 2.0_f64).into(); // uses From impl let p3 = Point::from((3.0, 4.0)); let p4 = Point::default(); // Point { x: 0.0, y: 0.0 } println!("Display: {}", p1); // (1, 2) println!("Debug: {:?}", p1); // Point { x: 1.0, y: 2.0 } println!("p1 == p2: {}", p1 == p2); // true println!("p1 == p3: {}", p1 == p3); // false println!("default: {}", p4); // (0, 0) let p_clone = p3.clone(); // Clone — independent copy println!("cloned: {}", p_clone); // (3, 4) }
UNDER THE HOOD
  • #[derive(Debug, Clone, Default)] — the compiler auto-generates these trait implementations by inspecting each field. Debug prints all fields, Clone clones each field, Default calls Default::default() on each field. Only works if every field's type also implements those traits.
  • From<(f64, f64)> for Point automatically gives you Into<Point> for (f64, f64) — Rust has a blanket impl: impl<T, U: From<T>> Into<U> for T. Implement From, get Into free.
  • Display vs Debug: Display ({}) is for end users — you control the format. Debug ({:?}) is for developers — auto-generated, shows internal structure. Frameworks like axum often require Display on error types; tracing uses Debug for field logging.
  • Implementing PartialEq manually lets you customize equality — e.g. case-insensitive string comparison, floating-point epsilon checks.
USE CASE — MY EXAMPLE The most important production use: traits as API contracts / seams. Define a Repository trait so your business logic depends on the abstraction, not the concrete database. This makes testing trivial — swap the real DB for a mock that also implements the trait.
day03_my_trait_as_contract.rs
PRODUCTION PATTERN
use std::collections::HashMap; // The contract — business logic depends ONLY on this trait trait UserRepository { fn get_user(&self, id: u32) -> Option<&str>; fn save_user(&mut self, id: u32, name: &str); } // REAL implementation — talks to a DB (simplified) struct PostgresRepo { conn_str: String } impl UserRepository for PostgresRepo { fn get_user(&self, id: u32) -> Option<&str> { println!("[postgres] querying {} for id={}", self.conn_str, id); None // simplified — real code sends SQL } fn save_user(&mut self, id: u32, name: &str) { println!("[postgres] INSERT user id={} name={}", id, name); } } // MOCK implementation — in-memory, used in tests struct MockRepo { store: HashMap<u32, String> } impl UserRepository for MockRepo { fn get_user(&self, id: u32) -> Option<&str> { self.store.get(&id).map(|s| s.as_str()) } fn save_user(&mut self, id: u32, name: &str) { self.store.insert(id, name.to_string()); } } // Business logic — generic over ANY UserRepository impl // The logic is the same regardless of real DB or mock fn create_user<R: UserRepository>(repo: &mut R, id: u32, name: &str) { if repo.get_user(id).is_none() { repo.save_user(id, name); println!("User {} created.", name); } else { println!("User {} already exists.", id); } } fn main() { // In tests — use MockRepo, no DB needed let mut mock = MockRepo { store: HashMap::new() }; create_user(&mut mock, 1, "Mishal"); // User Mishal created. create_user(&mut mock, 1, "Mishal"); // User 1 already exists. // In production — use PostgresRepo, same logic let mut pg = PostgresRepo { conn_str: "postgres://localhost/mydb".into() }; create_user(&mut pg, 2, "Alice"); }
UNDER THE HOOD
  • This is the Dependency Inversion Principle in Rust — high-level code (create_user) depends on an abstraction (the trait), not a concrete implementation. This pattern appears everywhere in production Rust: HTTP handlers, background workers, CLI tools.
  • create_user<R: UserRepository> — static dispatch. The compiler generates a version for MockRepo and another for PostgresRepo. Zero overhead at runtime.
  • If you need to store the repo in a struct (e.g. an axum handler state), you can use either a generic struct parameter or Box<dyn UserRepository>. The generic approach is faster; the Box<dyn> approach is more flexible (easier to store in collections, pass across threads with + Send).
  • In async Rust, this pattern extends naturally: trait UserRepository { async fn get_user(...) } — but async traits need the async_trait crate (Day 07 topic).
🔌

Dependency Injection via Traits

Traits are Rust's seam for testability. Business logic accepts impl Repository — swap a real DB for a fast in-memory mock in tests. No mocking framework needed.

Zero-Cost Generic Algorithms

Standard library sorts, serializers, and math ops are all generic. Your custom algorithms get the same zero-cost treatment via monomorphization — no Java-style boxing.

🔒

Plugin & Extension Systems

Box<dyn Trait + Send + Sync> powers plugin architectures — load unknown implementations at runtime (from config, reflection, or dynamic linking) through a fixed interface.

🧩

serde / axum / tokio Integration

Every framework integration in Rust works through traits: Serialize/Deserialize for serde, IntoResponse for axum, Future for tokio. Understanding traits unlocks the whole ecosystem.

REAL-WORLD USAGE
axum — every handler response type implements IntoResponse; extractors implement FromRequest. Adding a new type to your API just means impl-ing those traits.
serde#[derive(Serialize, Deserialize)] auto-implements the traits via proc macros. Custom types with manual impl blocks can control serialization completely.
tokio — the Future trait is how every async task works. impl Future for MyStateMachine is what async fn desugars to under the hood.
diesel / sqlx — generic over Connection traits; your query code works against any supported database backend through the same trait-based interface.
Q1 What is the difference between impl Trait and dyn Trait? +
impl Trait is static dispatch — the compiler resolves the exact type at compile time and monomorphizes the function, generating a separate version per concrete type. Zero runtime overhead, but return type must be a single concrete type.

dyn Trait is dynamic dispatch — the concrete type is resolved at runtime through a vtable (a table of function pointers). One function body handles all types. Slight overhead per call (one extra pointer indirection), but enables heterogeneous collections and runtime-determined types.

Rule of thumb: prefer impl Trait for performance; use dyn Trait when you need to store mixed types together or the concrete type is unknown until runtime.
Q2 What is monomorphization, and what is its trade-off? +
Monomorphization is the compiler process of generating a separate, concrete copy of generic code for each type it's used with. fn foo<T: Display>(x: T) called with i32 and String produces two distinct functions in the binary.

Advantage: zero runtime abstraction overhead — as fast as hand-written type-specific code; the optimizer can inline and specialize further.

Trade-off: binary size bloat — if a generic function is used with many different types, the binary contains many copies. dyn Trait avoids this with one shared function body.
Q3 What does "object safe" mean, and why does it matter for dyn Trait? +
A trait is object-safe if it can be used as a trait object (dyn Trait). The vtable for a trait object must have a fixed layout, so the compiler requires that:

1. No method returns Self by value (size of Self not known).
2. No method has generic type parameters (can't monomorphize per call through a vtable).
3. No associated functions without &self (can't dispatch through a pointer).

If a trait violates these, using Box<dyn MyTrait> gives a compile error. The fix is usually to remove the generic method from the trait and use a separate generic free function instead.
Q4 When would you use a where clause instead of inline trait bounds? +
where clauses are identical in semantics — purely a readability choice. Use them when:

1. You have multiple bounds on multiple type parameters — the angle brackets become unreadably long.
2. You need to bound associated types: where T::Item: Send + Sync.
3. You want to keep the function signature clean — put the bounds separately.

A rule of thumb: one or two simple bounds → inline. Three or more, or any associated type bounds → where.
Q5 What are default trait implementations, and how do they help? +
A trait can provide a default implementation for any of its methods — types that implement the trait can override it or just use the default for free. This lets you add behaviour to new types without requiring them to implement every method from scratch.

Real example: the Iterator trait has only one required method (next) — all 80+ other methods (map, filter, collect, etc.) are default implementations built on top of next. Implement next for your custom type and you instantly get the entire iterator API for free.
Q6 Can you implement a trait from another crate for a type from another crate? +
No — this is the orphan rule (coherence rule). You can only implement a trait if either the trait or the type is defined in your crate. You can't implement Display (from std) for Vec<i32> (from std) in your crate — both are foreign.

The workaround is the newtype pattern: wrap the foreign type in a local struct (struct MyVec(Vec<i32>)), then implement the foreign trait on your local newtype. The newtype is yours, so the orphan rule is satisfied.
Pattern What It Solves Used In
Associated Types
type Item
Bind an output type to a trait impl — cleaner than a type parameter when there's only one natural "output" type (e.g. Iterator::Item, Add::Output). Iterator, Add/Sub operators, async streams
Blanket Impls
impl<T: A> B for T
Implement a trait for ALL types satisfying some bound — the standard library does this for Into via From, and for ToString via Display. std conversions, derive-less trait propagation
Newtype Pattern Wrap a foreign type in a local struct to implement a foreign trait (orphan rule bypass). Also used to add invariants, restrict API surface, or add semantic meaning to primitives. Error wrapping, unit types (Metres(f64)), API design
Trait Specialization
(nightly)
Provide a more specific impl that overrides the blanket impl when a type satisfies additional constraints — not yet stable, but used in std internals. std string conversions, performance-optimized fast paths
HRTB on Trait Bounds
F: for<'a> Fn(&'a T)
Require a trait be implemented for all possible lifetimes — necessary when a callback is stored and called multiple times with different-lifetime inputs. Visitor patterns, iterator adapters, callback APIs
day03_advanced.rs — Associated Types + Blanket Impl
ADVANCED
// Associated type — cleaner than a type parameter when there's one natural output
trait Transformer {
    type Output;
    fn transform(&self) -> Self::Output;
}

struct Doubler(i32);
impl Transformer for Doubler {
    type Output = i32;
    fn transform(&self) -> i32 { self.0 * 2 }
}

struct Stringifier(i32);
impl Transformer for Stringifier {
    type Output = String;
    fn transform(&self) -> String { self.0.to_string() }
}

// Blanket impl — auto-implements Describe for ANYTHING that is Display
use std::fmt::Display;
trait Describe { fn describe(&self) -> String; }

impl<T: Display> Describe for T {
    fn describe(&self) -> String {
        format!("I am: {}", self)
    }
}

fn main() {
    println!("{}", Doubler(5).transform());      // 10 (i32)
    println!("{}", Stringifier(5).transform());  // "5" (String)

    println!("{}", 42_i32.describe());    // "I am: 42"  — blanket impl!
    println!("{}", "hello".describe());    // "I am: hello"
    println!("{}", 3.14_f64.describe());  // "I am: 3.14"
}