1 Month Learning Rust - Generic and Traits in Rust

1 Month Learning Rust - Generic and Traits in Rust

Daily short news for you
  • Since the Lunar New Year holiday has started, I won't be posting anymore. See you all after the holiday! 😁

    » Read more
  • Continuing about jj. I'm wondering if there are any GUI software made for it yet to make it easier to use. There are already so many similar to git that I can't count them all.

    Luckily, the author has compiled them all together in Community-built tools around Jujutsu 🥳

    » Read more
  • Turso announces that they are rewriting SQLite in Rust. This adds another piece of evidence supporting the notion that Rust is "redefining" many things.

    But the deeper reason is more interesting. Why are they doing this? Everyone knows that SQLite is open source, and anyone can create a fork to modify it as they wish. Does the Turso team dislike or distrust C—the language used to build SQLite?

    Let me share a bit of a story. Turso is a provider of database server services based on SQLite; they have made some customizations to a fork of SQLite to serve their purposes, calling it libSQL. They are "generous" in allowing the community to contribute freely.

    Returning to the point that SQLite is open source but not open contribution. There is only a small group of people behind the maintenance of this source code, and they do not accept pull requests from others. This means that any changes or features are created solely by this group. It seems that SQLite is very popular, but the community cannot do what they want, which is to contribute to its development.

    We know that most open source applications usually come with a "tests" directory that contains very strict tests. This makes collaboration in development much easier. If you want to modify or add a new feature, you first need to ensure that the changes pass all the tests. Many reports suggest that SQLite does not publicly share this testing suite. This inadvertently makes it difficult for those who want to modify the source code, as they are uncertain whether their new implementation is compatible with the existing features.

    tursodatabase/limbo is the project rewriting SQLite in Rust mentioned at the beginning of this article. They claim that it is fully compatible with SQLite and completely open source. Limbo is currently in the final stages of development. Let’s wait and see what the results will be in the future. For a detailed article, visit Introducing Limbo: A complete rewrite of SQLite in Rust.

    » Read more

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.

Premium
Hello

5 profound lessons

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

Every product comes with stories. The success of others is an inspiration for many to follow. 5 lessons learned have changed me forever. How about you? Click now!

View all

Subscribe to receive new article notifications

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

Comments (0)

Leave a comment...
Scroll or click to go to the next page