This article was originally posted by me on December 2, 2021, at thiscoindaily
Hello guys, welcome to the third and final part of the series where I introduce and take a deep dive into some rust techniques that every substrate developer should understand, in order to write smooth and hassle-free substrate codes.
In part two of this series, we went over generics. We also talked a little bit about traits and trait bounds. In this article, we’ll be taking a deep dive into traits and trait bounds. The aim of this article is to help you understand what traits are used for and equip yourself with the skills needed to understand how traits and trait bounds are used in your substrate code.
By the end of this article, you should be able to recognize and manipulate traits and their implementations in your substrate code and write your own custom traits and trait bounds for specific use-cases.
If you’re new to substrate/rust, you might want to check out this tutorial on getting started with Substrate.
Also, you should preferably have gone through the first and second parts of this series before going through this article (but that’s not mandatory).
Ready? Let’s get started!
Traits in the clearest terms
Essentially, a trait defines a set of methods that can be used by different types. For example, unlike native struct implementations which work for a specific struct, traits can be implemented in more than one structs. When a trait is implemented in a type, all the methods defined in the trait will be made available to the type.
If you’ve programmed in solidity before, traits in rust work almost the same way as interfaces in solidity.
A trait generally takes the form:
trait TraitName {
//for methods who's body would be implemented later
fn fn_name1 (&self) -> Type; //replace Type with the return type
//for default implementations
fn fn_name2 (&self) -> Return_Type {
//write function body here
}
}
Basically, every type that implements the trait above will have access to all the methods contained inside it.
To implement a trait for a certain type, the code structure will look like the below:
use TraitName for Type {
/* define trait functions here and write codes for their body. not that you don't have to define codes that have default implementation here, unless you want to override those implementation */
}
To get a clearer picture, let’s take a look at an example
Supposing we want to create a method that’s able to provide us with basic information about struct instances of Humans and Animals types. let’s call this method basic. Without traits, we would have to write the basic method for individual types. However, with traits, we only need to write one method. This method can then be applied to any type we wish.
Let’s look at how it works
To follow along, make sure you have a workspace set up. you can use rust playground if you don’t.
Let’s first define our structs for both Humans and Animals. If you don’t know how to create and work with structs, check out this article.
pub struct Human {
name: String,
age: i32,
location: String,
}
pub struct Animal {
name: String,
age: i32,
color: String,
}
Next, let’s define a trait that’ll house methods to be implemented in the structs above
pub trait About {
fn basic(&self) -> String;
}
Notice that the method defined in the trait above has no body. The body will be defined during implementation.
Now, let’s implement the trait for individual structs
impl About for Human {
fn basic(&self) -> String {
format!("the Human's name is {}, from {}", self.name, self.location)
}
}
impl About for Animal {
fn basic(&self) -> String {
format!("the Animal's name is {}, and its color is {}", self.name, self.color)
}
}
From the code above, you’ll notice that the body of the basic method has now been filled out.
the About trait is now implemented in the two structs, instances of either of the two structs will now have access to the basic method. check out the code below
fn main() {
let animal1 = Animal {
name: String::from("Dog"),
age: 3,
color: String::from("Black"),
};
let human1 = Human {
name: String::from("Abdulbee"),
age: 30,
location: String::from("Nigeria"),
};
println!("{}", animal1.basic());
println!("{}", human1.basic());
}
In the code above, we have instantiated two new structs of different types. we’ve also used the basic method to print out the basic information of these types. Despite the fact that the two types are different, the basic method is still able to work on them because the About trait (which contains the basic method) is implemented in both types.
if you run the code now, you should get the following output
the Animal's name is Dog, and its color is Black
the Human's name is Abdulbee, from Nigeria
Using traits as parameters
What if you want a single function that takes an instance of a struct and applies some traits methods to it?
In that case, you can use the About trait as a parameter for the function. That way the method will be applied to the argument provided it’s of a type that implements the About trait.
Enter this code below the main function
// using a trait as a parameter
pub fn retrieve_bio (var: &impl About) {
println!("{}", var.basic());
}
Because this function’s parameter is a trait, this function will only work for types that implement this trait. This is a simplistic version of a more robust technique known as trait bounds, which we shall look at later.
Now, replace the two print macros in your main function with the codes below and re-run the program. you should get the same results.
retrieve_bio(&animal1);
retrieve_bio(&human1);
Traits in substrate
A suitable example is the config trait, within which types and parameters are specified in the pallet. This trait can then be implemented for the runtime so that the runtime will have access to the contents of the trait.
Visit the pallet and runtime components of your substrate node’s source code (you can also use substrate playground) to see how the config trait is defined and implemented for the runtime.
If you really want to get your hands dirty and explore examples of traits and their implementations in substrate I’d suggest you read through the source code of some of substrate’s pallets.
How about trait bounds?
We’ve actually implemented a barebone version of trait bounds. Remember the previous coding example where we used the About trait as a parameter to implement the retrieve_bio function? Also, remember that this function will only work with types that have the About trait implemented on them. This is a simplistic representation of how trait bounds work.
Trait bounds attach a trait to a generic. This ensures that the types that have this trait bound to them, have access to all the contents of this trait. If you don’t know what generics are, be sure to check the second part of this series
Generally, trait bounds are created by adding a “:” in front of the T and then specifying the traits to bound (each trait should be separated by a plus sign. see example below
fn fn_name<T: trait1 + trait2>(var: T) -> T {
//
}
Let’s apply this to the previous example. Replace the retrieve_bio function with the code below, and re-run the program.
pub fn retrieve_bio<T: About> (var: &T) {
println!("{}", var.basic());
}
Notice that the program works the same. The only difference is, in this case, we bound the trait to the function itself, not to a single parameter. Hence, if we have more than one parameter in the function, using a trait bound ensures that all the parameters in the function inherit this generic.
Note that you can use multiple trait bounds in the same type, just separate them with a comma. Your code could get cumbersome when you use multiple trait bounds for a single type. To make your code more readable, it might be a good idea to use the were clause to help fractionate your code.
You can find tons of examples of trait bounds usage in substrate pallets. You should go through some of them, as they’ll come in handy when it comes to writing your own custom pallets.
There’re more advanced usages of traits that you’re less likely to encounter for now as a substrate developer. These include things like associated types, supertraits, etc. These are more advanced concepts and beyond the scope of this series. If you’re interested in learning about them, feel free to check them out
For now, the most important thing is to ensure that you understand traits/traits bounds and how they can be implemented in your substrate code. I’d suggest you read as much of other people’s substrate codes as you can, so as to expose yourself to loads of these implementations. Then, you can fetch out patterns from these codes, and apply them to your own substrate project.
Conclusion
This is the last of this series. I hope by now, you have a better idea of some core rust concepts used in Substrate and how to read, write, and understand them. Understanding these techniques (together with an exploration of tons of Substrate codes to hunt for implementations of these concepts) Will really go a long way in helping you master your skills as a Substrate Developer.
Keep learning, keep building!