Rust Traits: Deep Dive

Reading Time: 3 minutes

Traits are the abstract mechanism for adding functionality to Types or it tells Rust compiler about functionality a type must provide. In a nutshell, Traits are Interfaces of other languages.

In this article, I’ll talk about some deep concepts of Traits in Rust Programming. Before delving into this article please refer to this Rust Traits: Quick Introduction to understand the basics of Traits.

Now, let’s start with the Beyond Basic concepts of Traits as mentioned above, so the concepts which we gonna cover are:

  • Passing Trait as a Function’s Parameters
  • Trait Bound Concept
  • Return Trait Type from Function

Before proceeding with the above concepts lets understand some different aspects of traits that make them different from interfaces:

Implement Traits for Existing Types:

Unlike interfaces in languages like Java, or other languages, new traits can be implemented for existing types(existing libraries). For example, we can implement Rust Trait for existing types like bool, f32, etc.

trait DAY {
    fn sunday(&self) -> u64;
}

impl DAY for bool {
    fn sunday(&self) -> u64 {
        if *self { 0 } else { 1 }
    }
}

This is how we can implement the trait for any existing type. In the above example there is a trait called DAY and with one method called sunday(). So it is user-defined trait and then we are implementing it with the existing type which is bool and then we have provided the definition of sunday().

Access Methods Declared in the Same Trait

We can access other methods declared in the same trait.
Like in Traits we can provide method definition, so in the method’s definition, we can use other or access other methods defined in the same Trait.

trait Calculator {
    fn add(&self) -> u32;
    // We can provide default method definitions.
    fn get_result(&self) {
        println!("Result of Add() is {}", self.add());
    }
}

So, this is how we can access methods defined in the same trait. In the above example, we have two methods called add() and get_result(). Then we have used add() method in the definition of get_result() method.

Okay, let’s take over the listed concepts for which this article majorly belongs to.

Passing Trait as a Function’s Parameters

Passing trait into a function’s parameter is a quite interesting concept in the traits environment so, with the help of this user can put the restriction into his functionality like only limit functions can able to use it. For example:

/// pub struct Calculation {
///   first_num: u32,
///   second_num: u32
/// }
///
/// impl Calculator for Calculation {
///   fn add(&self) -> i32 {
///     self.first_num + self.second_num
///   }
/// }

pub fn calculate(item: impl Calculator) {
    println!("Addition {}", item.add());
}

/// let data: Calculation = Calculation {
///  first_num = 10,
///  second_num = 20
/// };
///
/// calculate(data); => valid
/// calculate(10);   => invalid

This is how a user can put restriction into his functionality. So here we set the function’s parameter for a specified Trait. Here this impl Calculator accepts only those types that implemented the Calculator Trait.

Trait Bound Concept

Trait Bound Concept is quite similar to the Passing Trait as a Function’s Parameters but with some syntactical differences.

pub fn calculate<T: Calculator> (item: T) {
     println!("Addition {}", item.add());
 }

This is how Trait Bound can be represented. Its syntax might look longer than a previous concept in some cases but it plays more efficient role than the previous one(in respect of syntax) if a user has multiple parameters of the same type. For example:

/// Directly setting parameters to the function. (Previous concpet)
pub fn calculate(item: impl Calculator, data: impl Calculator) {
    println!("Addition {}", item.add());
}

/// Trait Bound Concept
pub fn calculate<T: Calculator>(item: T, data: T) {
    println!("Addition {}", item.add());
}

This is how Trait bound looks different from Passing as a Parameters Concept. There are some different scenarios in Trait Bound are:

  • + operator
  • where clause

[+] operator is used to add more trait bound. For example:

pub fn calculate<T: Calculator + Any other Trait> (item: T) -> u32{}

where clause is used to simplify the concept of Trait Bound like:

/// without where clause
pub fn calculate<T: Calculator, U: Display> (item: T, data: Display) -> u32{
/// function's stuff
}

///with where clause
pub fn calculate<T, U> (item: T, data: Display) -> u32 
   where T: Calculator,
         U: Display
{
/// function's stuff
}

Here, where clause helps to identify the Trait Bounds while using multiple traits for different types.

Hope you all get acquainted with Traits.
Thanks for reading!


Knoldus-blog-footer-image

Written by 

Pawan Singh Bisht is the Trainee Software Consultant at Knoldus Software LLP. He has good knowledge of languages C++ and Java, and currently working on Rust. He is always ready to accept new challenges. As a fresher, he always tries to explore the different type of software and tools.

Leave a Reply

Knoldus Pune Careers - Hiring Freshers

Get a head start on your career at Knoldus. Join us!