Debug School

rakesh kumar
rakesh kumar

Posted on

How to reduce code duplication using Generics in Rust

Generics in Functions
Generics in Implementation (impl)
Traits
Bounds (Trait Bounds)
Multiple Bounds
Big Picture (one-line intuition)
Rust vs C / C++ — Concept Mapping (Big Picture)

Generics is the topic of generalizing types and functionalities to broader cases. This is extremely useful for reducing code duplication in many ways, but can call for rather involved syntax. Namely, being generic requires taking great care to specify over which types a generic type is actually considered valid. The simplest and most common use of generics is for type parameters.

A type parameter is specified as generic by the use of angle brackets and upper camel case: . "Generic type parameters" are typically represented as . In Rust, "generic" also describes anything that accepts one or more generic type parameters . Any type specified as a generic type parameter is generic, and everything else is concrete (non-generic).

Big Picture (one-line intuition)

Rust vs C / C++ — Concept Mapping (Big Picture)

Generics in Functions

When you need it

Use generic functions when:

Logic is the same

Type can vary

You don’t need stored state

Theory

fn func<T>(x: T) -> T
Enter fullscreen mode Exit fullscreen mode

T is decided at compile time.

Example

fn identity<T>(x: T) -> T {
    x
}

fn main() {
    println!("{}", identity(10));
    println!("{}", identity("Rust"));
}
Enter fullscreen mode Exit fullscreen mode

Output

10
Rust

Enter fullscreen mode Exit fullscreen mode

✅ Use when: only behavior varies, not data structure

14.2

Generic identity function

fn identity<T>(x: T) -> T { x }

fn main() {
    println!("{}", identity(10));
    println!("{}", identity(3.5));
}
Enter fullscreen mode Exit fullscreen mode

Output

10
3.5
Enter fullscreen mode Exit fullscreen mode

Generic swap (returns tuple)

fn swap<T, U>(a: T, b: U) -> (U, T) {
    (b, a)
}

fn main() {
    let s = swap("Rust", 2026);
    println!("{:?}", s);
}
Enter fullscreen mode Exit fullscreen mode

Output

(2026, "Rust")
Enter fullscreen mode Exit fullscreen mode

Generic max (needs PartialOrd)

fn max_of<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}

fn main() {
    println!("{}", max_of(10, 20));
    println!("{}", max_of(5.5, 3.2));
}
Enter fullscreen mode Exit fullscreen mode

Output

20
5.5
Enter fullscreen mode Exit fullscreen mode

Generic slice first element (returns Option)

fn first<T>(slice: &[T]) -> Option<&T> {
    slice.get(0)
}

fn main() {
    let a = [1, 2, 3];
    println!("{:?}", first(&a));

    let b: [i32; 0] = [];
    println!("{:?}", first(&b));
}
Enter fullscreen mode Exit fullscreen mode

Output


Some(1)
None
Enter fullscreen mode Exit fullscreen mode

Generic print length (accept anything that can be seen as slice)

fn print_len<T>(slice: &[T]) {
    println!("len={}", slice.len());
}

fn main() {
    print_len(&[10, 20, 30]);
    print_len(&['a', 'b']);
}
Enter fullscreen mode Exit fullscreen mode

Output

len=3
len=2
Enter fullscreen mode Exit fullscreen mode

Generics in Implementation (impl)

When you need it

Use generic impl when:

You want a type to store different data

Methods should work for all those types

Theory

struct Boxed<T> { value: T }
impl<T> Boxed<T> { ... }

Example
#[derive(Debug)]
struct Boxed<T> {
    value: T,
}

impl<T> Boxed<T> {
    fn get(&self) -> &T {
        &self.value
    }
}

fn main() {
    let a = Boxed { value: 100 };
    let b = Boxed { value: "Rust" };

    println!("{:?}", a.get());
    println!("{:?}", b.get());
}
Enter fullscreen mode Exit fullscreen mode

Output

100
"Rust"
Enter fullscreen mode Exit fullscreen mode

✅ Use when: data itself is generic
Generic struct Boxed

.
#[derive(Debug)]
struct Boxed<T>(T);

impl<T> Boxed<T> {
    fn new(x: T) -> Self { Boxed(x) }
}

fn main() {
    let a = Boxed::new(10);
    let b = Boxed::new("Hello");
    println!("{:?}", a);
    println!("{:?}", b);
}
Enter fullscreen mode Exit fullscreen mode

Output


Boxed(10)
Boxed("Hello")
Enter fullscreen mode Exit fullscreen mode

2) Generic struct with getter

struct Holder<T> {
    value: T,
}

impl<T> Holder<T> {
    fn get(&self) -> &T {
        &self.value
    }
}

fn main() {
    let h = Holder { value: 99 };
    println!("{}", h.get());
}
Enter fullscreen mode Exit fullscreen mode

Output

99
Enter fullscreen mode Exit fullscreen mode

3) Generic pair struct

#[derive(Debug)]
struct Pair<T, U> {
    a: T,
    b: U,
}

impl<T, U> Pair<T, U> {
    fn new(a: T, b: U) -> Self { Pair { a, b } }
}

fn main() {
    let p = Pair::new("ID", 1001);
    println!("{:?}", p);
}
Enter fullscreen mode Exit fullscreen mode

Output

Pair { a: "ID", b: 1001 }
Enter fullscreen mode Exit fullscreen mode

4) Specialized impl for one type (impl Holder)

struct Holder<T> { value: T }

impl Holder<String> {
    fn len(&self) -> usize {
        self.value.len()
    }
}

fn main() {
    let h = Holder { value: "Ashwani".to_string() };
    println!("len={}", h.len());
}
Enter fullscreen mode Exit fullscreen mode

Output

len=7
Enter fullscreen mode Exit fullscreen mode

5) Generic method that changes type

#[derive(Debug)]
struct Wrap<T>(T);

impl<T> Wrap<T> {
    fn map<U, F>(self, f: F) -> Wrap<U>
    where
        F: FnOnce(T) -> U,
    {
        Wrap(f(self.0))
    }
}

fn main() {
    let w = Wrap(10).map(|x| x.to_string());
    println!("{:?}", w);
}
Enter fullscreen mode Exit fullscreen mode

Output

Wrap("10")
Enter fullscreen mode Exit fullscreen mode

Traits

When you need it

Use traits when:

You want shared behavior

Types are unrelated but act similarly

You want polymorphism

Theory

Trait = behavior contract

Example

trait Speak {
    fn speak(&self) -> &'static str;
}

struct Dog;
struct Cat;

impl Speak for Dog {
    fn speak(&self) -> &'static str { "Woof" }
}
impl Speak for Cat {
    fn speak(&self) -> &'static str { "Meow" }
}

fn main() {
    let d = Dog;
    let c = Cat;

    println!("{}", d.speak());
    println!("{}", c.speak());
}
Enter fullscreen mode Exit fullscreen mode

Output

Woof
Meow
Enter fullscreen mode Exit fullscreen mode

✅ Use when: types differ but behavior is same

Generic trait method describe()

trait Describe {
    fn describe(&self) -> String;
}

struct Car { brand: String }

impl Describe for Car {
    fn describe(&self) -> String {
        format!("Car brand: {}", self.brand)
    }
}

fn print_desc<T: Describe>(x: &T) {
    println!("{}", x.describe());
}

fn main() {
    let c = Car { brand: "Tesla".to_string() };
    print_desc(&c);
}
Enter fullscreen mode Exit fullscreen mode

Output

Car brand: Tesla

2) Trait implemented for multiple types

trait Area {
    fn area(&self) -> i32;
}

struct Square(i32);
struct Rect(i32, i32);

impl Area for Square {
    fn area(&self) -> i32 { self.0 * self.0 }
}
impl Area for Rect {
    fn area(&self) -> i32 { self.0 * self.1 }
}

fn main() {
    let s = Square(5);
    let r = Rect(4, 6);
    println!("{}", s.area());
    println!("{}", r.area());
}
Enter fullscreen mode Exit fullscreen mode

Output

25
24
Enter fullscreen mode Exit fullscreen mode

3) Generic trait with associated type

trait Convert {
    type Output;
    fn convert(&self) -> Self::Output;
}

struct Meter(i32);

impl Convert for Meter {
    type Output = i32;
    fn convert(&self) -> i32 { self.0 * 100 } // to cm
}

fn main() {
    let m = Meter(2);
    println!("{}", m.convert());
}
Enter fullscreen mode Exit fullscreen mode

Output

200
Enter fullscreen mode Exit fullscreen mode

4) Generic function returning impl Trait

trait Greeting {
    fn greet(&self) -> String;
}

struct User { name: String }

impl Greeting for User {
    fn greet(&self) -> String {
        format!("Hello {}", self.name)
    }
}

fn create_user(name: &str) -> impl Greeting {
    User { name: name.to_string() }
}

fn main() {
    let u = create_user("Ashwani");
    println!("{}", u.greet());
}
Enter fullscreen mode Exit fullscreen mode

Output

Hello Ashwani
Enter fullscreen mode Exit fullscreen mode

5) Generic over iterator trait

fn count_items<I>(iter: I) -> usize
where
    I: IntoIterator,
{
    iter.into_iter().count()
}

fn main() {
    println!("{}", count_items(vec![1, 2, 3]));
    println!("{}", count_items([10, 20]));
}
Enter fullscreen mode Exit fullscreen mode

Output

3
2
Enter fullscreen mode Exit fullscreen mode

Bounds (Trait Bounds)

When you need it

Use bounds when:

Generic code needs capabilities

Compiler must guarantee behavior

Theory

T: Display
Enter fullscreen mode Exit fullscreen mode

Means: T must implement Display

Example

use std::fmt::Display;

fn show<T: Display>(x: T) {
    println!("{}", x);
}

fn main() {
    show(10);
    show("Rust");
}

Enter fullscreen mode Exit fullscreen mode

Output


10
Rust
Enter fullscreen mode Exit fullscreen mode
❌ Without bounds → compiler error
✅ With bounds → safe & explicit
Enter fullscreen mode Exit fullscreen mode

Bound with Display

use std::fmt::Display;

fn show<T: Display>(x: T) {
    println!("Value: {}", x);
}

fn main() {
    show(10);
    show("Rust");
}
Enter fullscreen mode Exit fullscreen mode

Output

Value: 10
Value: Rust
Enter fullscreen mode Exit fullscreen mode

Bound with Clone

fn duplicate<T: Clone>(x: T) -> (T, T) {
    (x.clone(), x)
}

fn main() {
    let d = duplicate(String::from("Hi"));
    println!("{:?}", d);
}
Enter fullscreen mode Exit fullscreen mode

Output

("Hi", "Hi")
Enter fullscreen mode Exit fullscreen mode

Bound with PartialOrd (comparison)

fn min_of<T: PartialOrd>(a: T, b: T) -> T {
    if a < b { a } else { b }
}

fn main() {
    println!("{}", min_of(10, 7));
}
Enter fullscreen mode Exit fullscreen mode

Output

7
Enter fullscreen mode Exit fullscreen mode

Bound with Debug

use std::fmt::Debug;

fn debug_print<T: Debug>(x: T) {
    println!("{:?}", x);
}

fn main() {
    debug_print((1, "hello", true));
}
Enter fullscreen mode Exit fullscreen mode

Output

(1, "hello", true)
Enter fullscreen mode Exit fullscreen mode

where clause bounds style

use std::fmt::Display;

fn add_and_show<T>(a: T, b: T)
where
    T: std::ops::Add<Output = T> + Display + Copy,
{
    let c = a + b;
    println!("{}", c);
}

fn main() {
    add_and_show(10, 20);
}
Enter fullscreen mode Exit fullscreen mode

Output

30
Enter fullscreen mode Exit fullscreen mode

Multiple Bounds

When you need it

Use multiple bounds when:

One trait is not enough

Type must support multiple behaviors

Theory
T: Display + Clone

Example

use std::fmt::Display;

fn print_twice<T: Display + Clone>(x: T) {
    println!("{}", x.clone());
    println!("{}", x);
}

fn main() {
    print_twice(String::from("Rust"));
}
Enter fullscreen mode Exit fullscreen mode

Output

Rust
Rust
Enter fullscreen mode Exit fullscreen mode

✅ Use when: one capability is insufficient

Display + Clone

use std::fmt::Display;

fn print_twice<T: Display + Clone>(x: T) {
    println!("{}", x.clone());
    println!("{}", x);
}

fn main() {
    print_twice(String::from("Rust"));
}
Enter fullscreen mode Exit fullscreen mode

Output

Rust
Rust
Enter fullscreen mode Exit fullscreen mode

2) Debug + PartialEq

use std::fmt::Debug;

fn is_equal<T: Debug + PartialEq>(a: T, b: T) {
    println!("{:?} == {:?} => {}", a, b, a == b);
}

fn main() {
    is_equal(10, 10);
    is_equal("a", "b");
}
Enter fullscreen mode Exit fullscreen mode

Output

10 == 10 => true
"a" == "b" => false
Enter fullscreen mode Exit fullscreen mode

3) Multiple bounds with where

use std::fmt::{Debug, Display};

fn show_both<T>(x: T)
where
    T: Debug + Display,
{
    println!("Display: {}", x);
    println!("Debug:   {:?}", x);
}

fn main() {
    show_both(99);
}
Enter fullscreen mode Exit fullscreen mode

Output

Display: 99
Debug:   99
Enter fullscreen mode Exit fullscreen mode

4) Bounds on two generic types

use std::fmt::Display;

fn pair_print<T: Display, U: Display>(a: T, b: U) {
    println!("{} {}", a, b);
}

fn main() {
    pair_print("ID", 5001);
}
Enter fullscreen mode Exit fullscreen mode

Output

ID 5001
Enter fullscreen mode Exit fullscreen mode

5) Bounds with closure (FnOnce) + Clone

fn run<T, F>(x: T, f: F) -> T
where
    T: Clone,
    F: FnOnce(T) -> T,
{
    f(x.clone())
}

fn main() {
    let out = run(10, |n| n + 5);
    println!("{}", out);
}
Enter fullscreen mode Exit fullscreen mode

Output

15
Enter fullscreen mode Exit fullscreen mode

Top comments (0)