Understanding Generics and Traits in Rust

What is Generics

Generic programming’s goal is to improve code reusability and reduce bugs by allowing functions,structures and traits to have their types defined later.In practice,this means that an algorithm can be used with multiple different types,provided that they fulfill the constraints In Rust, functions,traits and data types can be generic. To parameterize the types in a new single function, we need to name the type parameter, just as we do for the value parameters to a function. You can use any identifier as a type parameter name. But we’ll use T because, by convention, type parameter names in Rust are short, often just a letter, and Rust’s type-naming convention is CamelCase. Short for “type,” T is the default choice of most Rust programmers.

Example

use std::fmt::Display;

// a generic function whose type parameter is constrained
fn generic_display<T:Display>(item:T){
    println!("{}",item);
}

// A generic Struct
struct Point<T>{
    x:T,
    Y:T,
}
// a generic enum
enum Option<T> {
    Some(T),
    None
}
fn main() {
    let a: &str = "42";
    let b: i64 = 42;
    generic_display(a);
    generic_display(b);
    let (x, y) = (4i64, 2i64);
    let point: Point<i64> = Point {
        x,
        y
    };
    // generic_display(point) <- not possible. Point does not implement Display
}

We can also use generic type definition in methods implemented on structs and enums. For example:

struct Point<T> {
    x: T,
    y: T,
}
<!-- we have to declare T just after impl so we can use T to specify
 that we’re implementing methods on the type Point<T>. By declaring T
  as a generic type after impl, Rust can identify that the type in 
  the angle brackets in Point is a generic type rather than a concrete type -->
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Generic type parameters in a struct definition aren’t always the same as those you use in that same struct’s method signatures. For example:

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

The purpose of this example is to demonstrate a situation in which some generic parameters are declared with impl and some are declared with the method definition. Here, the generic parameters X1 and Y1 are declared after impl because they go with the struct definition. The generic parameters X2 and Y2 are declared after fn mixup, because they’re only relevant to the method.

Traits

Traits are similar to a feature often called interfaces in other languages, although with some differences.

A trait defines functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way. We can use trait bounds to specify that a generic type can be any type that has certain behavior.

A Trait can be defined as:

pub trait Dog {
    fn bark(&self) -> String;
}

pub struct Labrador{}
impl Dog for Labrador {
    fn bark(&self) -> String {
    "wouf".to_string()
}
}
pub struct Husky{}
impl Dog for Husky {
    fn bark(&self) -> String {
    "Wuuuuuu".to_string()
   }
}

fn main() {
        let labrador = Labrador{};
        println!("{}", labrador.bark());
        let husky = Husky{};
        println!("{}", husky.bark());

}

By defining a Dog interface, all types that implement this trait in our program will be considered as being a Dog.

Sometimes it’s useful to have default behavior for some or all of the methods in a trait instead of requiring implementations for all methods on every type. Then, as we implement the trait on a particular type, we can keep or override each method’s default behavior.

Example

pub trait Dog {
    fn bark(&self) -> String{
        "Bark".to_string()
    }
}

Traits can also have generic parameters

use std::fmt::Display;
trait Printer<S: Display> {
        fn print(&self, to_print: S) {
        println!("{}", to_print);
    }
   }

struct ActualPrinter{}
impl<S: Display> Printer<S> for ActualPrinter {}


fn main() {
    let s = "Hello";
    let n: i64 = 42;
    let printer = ActualPrinter{};
    printer.print(s);
    printer.print(n);
}

The derive attribute

When you have a lot of traits to implement for your types, it can quickly become tedious and may complexify your code. To simplify the code,Rust has the derive attribute.

By using the derive attribute, we are actually feeding our types to a Derive macro which is a kind of procedural macro. They take code as input (in this case, our type), and create more code as output. At compile-time.

This is especially useful for data deserialization: Just by implementing the Serialize and Deserialize traits from the serde crate, the (almost) universally used serialization library in the Rust world, we can then serialize and deserialize our types to a lot of data formats: JSON, YAML, TOML, BSON and so on.

Example

use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Point {
    x: u64,
    y: u64,
}

Without much effort, we just implemented the Debug , Clone , Serialize and Deserialize traits for our struct Point.

One thing to note is that all the subfields of your struct need to implement the traits:

use serde::{Serialize, Deserialize};
// Not possible:
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Point<T> {
        x: T,
        y: T,
}
// instead, do this:
use serde::{Serialize, Deserialize};
use core::fmt::Debug; // Import the Debug trait
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Point<T: Debug + Clone + Serialize + Deserialize> {
        x: T,
        y: T,

}