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.
What is Trait
As defined in the book The Rust Programming Language, (you can find it at: 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:
Define a Trait
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++.
Define a Struct with a trade object
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: doc.rust-lang.org/book/ch17-02-trait-object..
Define different types and implement Trait
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.
Implement Trade type
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.
Puting it all together
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
Speaking of design patterns
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!