Alex Merced I am a developer, educator, and founder of devNursery.com.

Using generic impl blocks in Rust

4 min read 1374

Using Generic Impl Blocks In Rust

Rust generic implementation blocks make implementing methods for generic types much easier.

In this article, we’ll illustrate the value of generic implementation blocks by first understanding the tediousness of a world without generics, and the results from when we use them.

To jump ahead in this article:

Rust structs and impl blocks

Unlike languages like C++, JavaScript, and C#, in Rust, you don’t have objects (groups of properties and methods) or classes for defining types of objects.

Instead, Rust, like Go, opts for structs that are just groups of properties. In Rust, methods are added to structs by using implementation, and impl blocks are used for defining methods. Refer to the example below.

// Struct Definition
struct Person {
    name: String
}

// Impl block defining method
impl Person {
  fn say_name(&self){
    println!("{}", self.name);
  }
}


fn main() {
    // Instantiate a person
    let alex = Person {name: "Alex Merced".to_string()};

    // Call methods
    alex.say_name();
}

In the code above, we created a Person struct with one String property called name. Then, we used an impl block to define and implement a say_name method. In the main function, we created a Person struct and called the say_name() method, which will print Alex Merced.

Generic types

Sometimes, we have structs that may have properties whose types are defined differently. To designate that, we can list out the unknown types as variables like <T> or <T, U>, depending on how many variable types we need to designate.

Then, we need to implement any methods for each type we want to support. This can be very tedious if we are implementing a method for many types.

// Generic Struct Definition, T is a variable for unknown type
struct Stats<T> {
    age: T,
    height:T
}

// Impl blocks defining methods for different types
impl Stats<i32> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {} inches", self.age, self.height);
  }
}

impl Stats<f32> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {} feet", self.age, self.height);
  }
}


fn main() {
    // Instantiate using i32 stats
    let alex = Stats {age: 37, height: 70};
    // Instantiate using f32 stats
    let alex2 = Stats {age: 37.0, height: 5.83};
    // Call methods
    alex.print_stats();
    alex2.print_stats();
}

In the example code above, we define a Stats struct, which has two properties of the same type, denoted by T. Because we defined it as a generic type when we wrote <T>, this type can be anything, but it must be the same for age and height.

We want any Stats objects that use two 32 bit numbers, whether integers or floats, to have a print_stats method. In this case, we have to create two implementation blocks for each possibility.



We then instantiate two Stats structs to use i32 values and f32 values. We call the print_stats method for structs to get the following output:

Age is 37 years and Height is 70 inches
Age is 37 years and Height is 5.83 feet

Notice the different output because we called the implementation from the right implementation block.

Generic implementation blocks

Writing a separate implementation block for each type can get tedious, especially if there are several possibilities.

If the implementation for several types is going to be identical, we can use generic implementation blocks, which, like generic types, allow us to define variables representing the type:

use std::fmt::Display;

// Generic Struct Definition, T is a variable for unknown type
struct Stats<T> {
    age: T,
    height:T
}

// Impl blocks defining methods for different types
impl<T:Display> Stats<T> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {}", self.age, self.height);
  }
}


fn main() {
    // Instantiate using i32 stats
    let alex = Stats {age: 37, height: 70};
    // Instantiate using f32 stats
    let alex2 = Stats {age: 37.0, height: 5.83};
    // Instantiate using String stats
    let alex3 = Stats {age: "37".to_string(), height: "5'10ft".to_string()};
    // Call methods
    alex.print_stats();
    alex2.print_stats();
    alex3.print_stats();
}

In the code above, we imported the Display trait from the standard library because we’ll need to reference it later.

We defined the Stats struct as a struct with two properties of the same generic type, age, and height, respectively.

We used only one implementation block to define a generic type of <T:Display>, which means T is the variable for the generic type. :Display means it must be a type that implements the Display trait. This is required so we can use the println! macro in this implementation.

We then defined three structs: one using i32, another using f32, and the final one using strings as its property values. (We used three different types and only needed one impl block. How cool is that!)

When it runs, you should get the output below:

Age is 37 years and Height is 70
Age is 37 years and Height is 5.83
Age is 37 years and Height is 5'10ft

Implementing trains with generic impl blocks

Generic impl blocks can also be helpful when we need to implement a trait on many types. In this scenario, we can use a generic implementation to save us time, then define specific implementations as needed. You can see this in the code below.

use std::fmt::Display;

// Generic Struct Definition, T is a variable for unknown type
struct Stats<T> {
    age: T,
    height:T
}

// Generic Tuple Struct that holds one value
struct Number<T> (T);

// trait with default implementation of print_stats method
trait ViewStats {
  fn print_stats(&self){
    println!("The type of age and height doesn't implement the display trait")
  }
}

// Impl blocks defining methods for different types
impl<T:Display> ViewStats for Stats<T> {
  fn print_stats(&self){
    println!("Age is {} years and Height is {}", self.age, self.height);
  }
}

//Impl block to ViewStats trait to number but use default implementation
impl<T> ViewStats for Stats<Number<T>> {}


fn main() {
    // Instantiate using i32 stats
    let alex = Stats {age: 37, height: 70};
    // Instantiate using f32 stats
    let alex2 = Stats {age: 37.0, height: 5.83};
    // Instantiate using String stats
    let alex3 = Stats {age: "37".to_string(), height: "5'10ft".to_string()};
    // Instantiate using String stats
    let alex4 = Stats {age: Number(37), height: Number(70)};
    // Call methods
    alex.print_stats();
    alex2.print_stats();
    alex3.print_stats();
    alex4.print_stats();
}

In the code above, we imported the Display trait from the standard library. We defined the Stats struct as a struct with properties of the age and height of any types, except matching types.


More great articles from LogRocket:


Then, we defined a Number struct, which is a tuple with one value. This is just so we can demonstrate what happens when we create stats with a type that doesn’t implement display.

Next, we defined a ViewStats trait where the print_stats method is given a default implementation. This implementation will be called if the values of age and height are valid types but don’t have their own implementation.

Then, we defined an implementation of ViewStats for Stats<T>, where T is any type that implements the Display trait, like i32, f32, and String.

Then, we implemented ViewStats for Number. Because we don’t define a new version of print_stats, it will use the default one from the trait declaration block.

We then created four structs. The first three are the same as before, and the fourth one is a Stats struct where age and height are represented by Number structs.

When we run the script, we get the following output:

Age is 37 years and Height is 70
Age is 37 years and Height is 5.83
Age is 37 years and Height is 5'10ft
The type of age and height doesn't implement the display trait

Notice that the final line shows the result of our default implementation from the train block because Number<T> doesn’t implement display, so it can’t use the same implementation as the first three instances of Stats<T>.

Conclusion

Generics simplify writing code that should work with many types, whether it is by using generics to define property types or generic impl blocks to define methods without the tediousness of doing so repeatedly.

LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Alex Merced I am a developer, educator, and founder of devNursery.com.

Leave a Reply