1 Month Learning Rust - Generic and Traits in Rust

1 Month Learning Rust - Generic and Traits in Rust

Daily short news for you
  • openai/codex is the latest open-source project from OpenAI, following their announcement of the two newest models, o3 and o4 mini. It is said that both o3 and o4 mini are very suitable for being Agents, so they released Codex as a lightweight Agent that runs directly in the Terminal.

    Regarding its applicability, since it is an Agent, it can read/add/edit/delete the contents of your files. For example.

    codex "explain this codebase to me"

    Or integrate it into your CI/CD pipeline.

    - name: Update changelog via Codex run: | npm install -g @openai/codex export OPENAI_API_KEY="${{ secrets.OPENAI_KEY }}" codex -a auto-edit --quiet "update CHANGELOG for next release"

    Oh, I almost forgot, you need to use the OpenAI API 😆

    » Read more
  • Perhaps many people do not know that OpenAI has launched its own academy page to help users learn and fully harness the power of their language models.

    OpenAI Academy

    » Read more
  • Mornings have started with some sensational news: OpenAI wants to acquire Windsurf for $3 billion 😳

    » 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

Me & the desire to "play with words"

Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member now!

Have you tried writing? And then failed or not satisfied? At 2coffee.dev we have had a hard time with writing. Don't be discouraged, because now we have a way to help you. Click to become a member 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...