1 Month Learning Rust - Generic and Traits in Rust

1 Month Learning Rust - Generic and Traits in Rust

The Problem

JavaScript is one of the languages that do not need to declare the type. The data type is automatically implied and can change flexibly. A variable initially assigned a value as a number, just after a few lines of code it can become a string or any other value. That's interesting and sometimes a disaster!

In Rust, as well as in many typed programming languages, we are forced to declare the type for almost everything like variables, functions… In short, the data type is mandatory and you cannot change from one type to another like in JavaScript.

This has both advantages and disadvantages. When everything is clear, we can avoid many unnecessary errors, and it's also easier for others to understand what an object holds. But on the other hand, you have to spend a lot of time declaring types and lose the flexibility like in JavaScript.

Let's put that aside for now, because the above is just a personal opinion. Whether you like it or not depends on each individual and each situation. But Rust is definitely a typed language, and you must know about Generic and Traits to know how to handle some common cases.

Generic

We use generic to create definitions for functions or structs, and then use them with specific data types.

Take a simple example, largest is a function that finds the largest number in a list with the type i32.

fn largest(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

largest takes a list with the type i32, and after finding the largest number, it returns that number. But in reality, besides i32, we have many other data types that can find the largest value like i64, f32, f64, char… Obviously, largest cannot take a list with a different data type than i32. Do we have to create another function with a new data type every time we want to find the largest number?

Generics in Rust help us solve this problem of code duplication. A Generic is declared with uppercase letters to represent a general value. For example:

fn largest<T>(list: &[T]) -> &T {
    ...
}

It's easy to see that T here represents a general value instead of a specific data type. If we read it, it would be: "the largest function takes a list with the data type T and returns a value also with the data type T".

The new function implementation will look like this:

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

However, largest still cannot work because T is too general. Let's look at the function body, the comparison item > largest makes the function not work if we pass in data types that cannot be compared like struct, enum… To solve this, in Rust, we have the concept of traits - PartialOrd is a trait and we only need to declare T as PartialOrd.

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

So what is a trait?

Traits

Traits are a concept to group similar objects. Traits are similar to interfaces in object-oriented programming languages.

For example, you have two objects NewsArticle used to store a news article and Tweet to store a discussion topic. You realize that both objects need a summarize function to summarize the content instead of displaying a long content. Traits appear:

pub trait Summary {
    fn summarize(&self) -> String;
}

Summary is declared as a trait and inside there is a method summarize. Looking at it, we only see that it returns a String and does not see any implementation code. That's right, because this is just a trait declaration.

Assuming NewsArticle and Tweet have the following structures:

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

Then to implement the trait, we need to do:

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

As a result, whenever we call the summarize function from NewsArticle or Tweet, we will get the corresponding result.

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize()); // 1 new tweet: horse_ebooks: of course, as you probably already know, people
}

Why not implement the summarize function directly in NewsArticle or Tweet but have to go through traits? It's to declare the type and use it to support parameters with multiple data types in a function.

Like the example above, when we didn't declare T as PartialOrd, the comparison inside would fail. On the other hand, T is PartialOrd, PartialOrd implements the comparison for T so Rust knows how to compare T with each other. Or like the example below:

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

notify takes an item parameter that is the type that implements the Summary trait. item can be any object that implements the Summary trait, then item.summarize() will work.

or
* The summary newsletter is sent every 1-2 weeks, cancel anytime.
Author

Hello, my name is Hoai - a developer who tells stories through writing ✍️ and creating products 🚀. With many years of programming experience, I have contributed to various products that bring value to users at my workplace as well as to myself. My hobbies include reading, writing, and researching... I created this blog with the mission of delivering quality articles to the readers of 2coffee.dev.Follow me through these channels LinkedIn, Facebook, Instagram, Telegram.

Did you find this article helpful?
NoYes

Comments (0)