Traits: Rust's take on interfaces

Search for a command to run...

No comments yet. Be the first to comment.
This blog series explores the depth and versatility of the Rust programming language through powerful lenses such as Object-Oriented Programming (OOP), Functional Programming (FP), and Design Patterns.
Don't panic! Rust has Results
Iterators are a fundamental concept in programming that enable you to process a sequence of items, such as elements in a collection, one at a time, without revealing the collection's internal structure. They form the backbone of operations like loopi...

A closure is an anonymous function that can capture variables from its environment. At a high level, Rust closures allow you to write small, concise functions inline, without having to formally define a new fn with a name. They’re particularly handy ...

Immutability is a cool concept in programming that C++ questions with "why?" It means that once you create a variable or object, you can't change its state. This idea somewhat opposes object-oriented concepts, yet it's crucial for writing clean, main...

Object-oriented programming, also called OOP, is a programming style that is dependent on the concept of objects. It is very popular and established. It is like designing and organizing your code by thinking of parts of your program as real-world obj...

Traits are the most important topic to understand the design patterns in Rust. Traits are fundamental feature in Rust that provide a way for shared behavior, abstraction and polymorphish. You can think of traits as interfaces in other programming la...

We know structs represents complex data types. Rust's struct acts more like classes in Python and C++ once we implements struct using impl keyword and you have self to access structs attributes. However, struct doesn't support inheritance. So in that sense, you can say Rust is not an Object-oriented language. You can not define an abstract base struct and inherit to make a derived struct and probably another derived struct from derived one and make the code as unreadable and complex as possible for the developers like me. At least one good news about learning Rust is Rust doesn't have inheritance.
That's great, however, the problem is, I learned design patterns a week ago from the awesome book Head First Design patterns. These concepts seem pretty universal, but then how do we apply them in Rust?
The answer is Trait objects and Generics.
Let's focus on trait object for this discussion.
As defined in the book The Rust Programming Language, (you can find it at: https://doc.rust-lang.org/stable/book)
A trait tells the Rust compiler about functionality a particular type has and can share with other types. We can use traits to define shared behavior in an abstract way.
To achieve the polymorphism in Rust, you need a pointer (reference) that can hold any references of any type that have shared behavior. We will use the dyn keyword for this pointer.
Let's understand this with a simple example where we will create type of financial contracts and a trade that can refer to different types of financial contracts:
We have a variety of financial instruments from different asset classes like equity, equity options, and interest rate derivatives. We need a few generic methods on all of the contracts so that we can analyze the portfolio of trades.
We define a trait named Contract with one method named npv.
pub trait Contract{
fn npv(&self);
}
The above function may look like a virtual method in C++.
Let's create a struct named Trade. The idea is to store our traded financial contracts. Here we use a trait object that allows different types of contracts.
pub struct Trade{
contract: Box<dyn Contract>
}
dyn Contract is a trade object. Box<dyn Contract> is a fat pointer that consists of a pointer to the value, plus a pointer to a table representing a value type. This table is the same as a C++ virtual table (vtable). Our Trade type can hold the reference to any structs that implement the Contract trait.
Refer: https://doc.rust-lang.org/book/ch17-02-trait-objects.html
Let's just create two instruments that can be widely different. We don't have to go into detail of swap and swaption for this example. We have Swap and Swaption, both implementing Contract.
pub struct Swap;
impl Contract for Swap {
fn npv(&self) {
println!("This is npv of Swap")
}
}
pub struct Swaption;
impl Contract for Swaption {
fn npv(&self) {
println!("This is npv of Swaption")
}
}
The syntex impl TraitName for Type is used to implement a trait. This impl block must have only the features of the trait. For other methods, you need to use different impl blocks.
impl Trade {
pub fn new() -> Self {
Trade {
contract: Box::new(Swap),
}
}
pub fn npv(&self) {
self.contract.npv();
}
pub fn set_contract(&mut self, contract: Box<dyn Contract>){
self.contract = contract;
}
}
We define a new method to create a Swap by default. Rust does not have constructors too, instead, the convention is to use an associated function new to create an object: We have an additional set_contract method that takes the trait object and updates the contract. We also define the npv that calls the trait object's npv based on run-time polymorphism.
let mut trade = Trade::new();
trade.npv();
println!("Setting new contract");
trade.set_contract(Box::new(Swaption));
trade.npv();
This is npv of Swap
Setting new contract
This is npv of Swaption
We just used the strategy design pattern to calculate the npv of trades. Contract Traits makes different types (Swap and Swaption) interchangeable inside the Trade type. In the words of HFDP
The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it. Thank you for reading this, and good luck with your Rust and design patterns!