Skip to main content

Defining behavior with Traits

There is no genuine concept of object-oriented-style inheritance in Rust. Traits introduce the notion of defining shared behavior for data structures. A trait defines a set of shared functions, expectations, and behavior that can be used for an indefinite number of types.

Creating a Trait

Defining a trait utilizes the trait keyword, followed by the name of the trait. In this example, notice the use of the pub (public) keyword. This exposes the trait to any external files within a Rust crate and project to utilize:

pub trait Transferrable {
fn transfer(&mut self, who: &mut Self, amount: i32) -> i32;
}

This function defines a trait Transferrable, which also defines a method signature, called transfer. This signature is a blueprint for what we expect from any type that implements Transferrable as a trait. As implied by the name, this trait would allow a type to send currency from the caller to who. The use of this trait would allow for a particular type to now possess these methods in order to achieve this functionality.

In order these methods to be used, however, they must be implemented on that specific type.

info

Take note of the difference of usage between self and Self. Remember, self refers to an already instantiated instance of that type, whereas Self refers to simply that type. The use of Self here allows for this trait to look for the specific type being implemented when it comes to who should receive currency.

Common Traits in Rust

As you may have already seen, there are quite a few traits that are used in Rust. A few common traits are:

  • Debug - formats the output in a debugging context.
  • PartialEq - A trait for equality comparisons.
  • Clone - Describes how a new value can be created, or "Cloned".

There are many more, but these, when used in conjunction with the derive macro with structs, can be very useful in making structs more pleasant to handle.

Using the derive macro

A line you may have encountered above a struct declaration is #[derive()]. This is what is called a procedural macros, which create a sort of auto-implementation for a set of compatible traits. Between the parentheses, any traits compatible with this macro, along with the struct's fields within can be implemented:

#[derive(PartialEq)]
struct Stormtrooper {
name: String
}

Because String also implements PartialEq, this is perfectly acceptable and easier than fully writing out the implementation for this particular trait. It's a shorthand way of implementing common traits within the standard library. Without PartialEq in this context, we wouldn't be able to utilize the equality operator (==).

let stormtrooper_one = StormTrooper { "Stormtrooper 1".to_string() };
let stormtrooper_two = StormTrooper { "Stormtrooper 2".to_string() };

// Possible due to PartialEq! No need for a full trait implementation.
println!("Is Stormtrooper One equal to Stormtrooper Two: {}", stormtrooper_one == stormtrooper_two);

Implementing a Trait on an "Account" struct

To implement (impl) this trait, we will create a struct called Account, which will also be marked as pub:

note

Here, the concept of an Account is more in the context of a blockchain, where an account has some identification (id, in this case, although this could also be an address) and a monetary balance. This theme will become more present throughout the course, as these terms will become more prevalent when learning more about developing with Substrate.

pub struct Account {
pub id: i32,
pub balance: i32,
pub is_legit: bool,
}

impl Transferrable for Account {}

If the above code was run, we'd be presented with this error:

error[E0046]: not all trait items implemented, missing: `transfer`
--> src/main.rs:11:1
|
8 | fn transfer(&mut self, who: &mut Self, amount: i32) -> i32;
| ------------------------------------------- `transfer` from trait
...
11 | impl Transferrable for Account {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `transfer` in implementation

For more information about this error, try `rustc --explain E0046`.

As the compiler clearly says - we must implement all method signatures for this implementation!

Now, we may implement our Transferrable trait to properly reflect the method signatures:

impl Transferrable for Account {
fn transfer(&mut self, who: &mut Self, amount: i32) -> i32 {
// Take from our (self) balance,
self.balance = self.balance - amount;
// Add that amount to someone else's balance,
who.balance = who.balance + amount;
// return our remaining balance.
self.balance
}
}

Because we implemented Transferrable with this type, now any instance of the account struct can utilize the transfer() method and ensure shared behavior:

    let mut alice = Account {
id: 1,
balance: 100,
is_legit: true,
};

let mut bob = Account {
id: 2,
balance: 200,
is_legit: true,
};

// In theory, account two could also easily call this!
alice.transfer(&mut bob, 50);

println!(
"Account One Balance: {}, Account Two Balance: {}",
alice.balance, bob.balance
);

In fact, any type that impls Transferrable can now transfer funds, so long as it's implemented correctly as per the method and trait signatures.

To illustrate this let us add a method signature to Transferrable to drain all funds from an Account:

pub trait Transferrable {
fn transfer(&mut self, who: &mut Self, amount: i32) -> i32;
/// New method signature! Now we *have* to implement it.
fn drain_funds(&mut self) -> bool;
}

impl Transferrable for Account {
fn transfer(&mut self, who: &mut Self, amount: i32) -> i32 {
// Take from our (self) balance,
self.balance = self.balance - amount;
// Add that amount to someone else's balance,
who.balance = who.balance + amount;
// return our remaining balance.
self.balance
}

fn drain_funds(&mut self) -> bool {
// Drain it all!
self.balance = 0;
// Really, we should have a Result that ensures that the account was successfully drained.
true
}
}
note

While drain_funds returns a bool for simplicity, a Result<T, E> would be more appropriate here, along with more robust checking on if the user is allowed to drain their funds or not.

And as before, it's as simple as calling the drain_funds on any type, in this case, Account, that implements the Transferrabletrait:

// Account two's balance is now 0
bob.drain_funds();

Feel free to define more types and implement, or even expand Transferrable with more methods as you see fit.

Trait Parameters & Bounds

A common use of traits is their use as bounds in functions, which can then be used to define parameters that abide by these bounds. With generics, this becomes possible and produces very reusable code:

info

For this scenario, we added an additional method to the Transferrable trait - is_legit(), which verifies the legitimacy of a particular entity. This entity could be an Account, or even something like a SmartContract type.

fn verify_entity(entity: &impl Transferrable) {
if entity.is_legit() {
println!("Entity is legit!");
} else {
println!("Entity is NOT legit!");
}
}

The function's parameter, entity, is saying something very particular here: any type which implements Transferrable as a trait is a valid parameter. If we were to define another type, say SmartContract, that implemented Transferrable, then that would also be completely valid.

It can also be written using trait bound syntax, which allows for a better view of generics at work:

fn verify_entity<T: Transferrable>(entity: &T) {
if entity.is_legit() {
println!("Entity is legit!");
} else {
println!("Entity is NOT legit!");
}
}

To call a function like this would be quite interesting, as we would need to specify which type, so long as it abides by the trait bound Transferrable:

let mut account = Account {
id: 1,
balance: 100,
};
// Account goes in the angled brackets, as it's a type 'T' that implements 'Transferrable':
verify_entity::<Account>(&account);

Try it yourself!

What's happening here?

In this example, we define a common trait Transferrable, which allows a struct to define and access three methods: transfer, is_legit, and drain_funds. When a struct implements this trait, it must specify the functionality for each method. A function, verify_entity, also introduces a trait bound T: Transferrable. This bound ensures that only types that have correctly implemented Transferrable are to be valid arguments.