Adventures in Rust

I should learn Rust. 🐱👓📰
lang-eng
rust
Published

October 29, 2022

1 Introduction

And I’m back to Rust.

Why? 2 reasons:

  • I want to learn statically typed language. Alternatives were C++ (which I considered … and rejected) and C#. C# would be a good alternative, but I don’t know: I don’t think I would use it in my daily job too much. Rust, on the other hand, can speed up my Python code when I need it.
  • Rust offers new philosophy of thinking and code solving, and I want to get immersed into it.

This resource offers great introduction for Python developers, and I currently got to about 20% of the book.

This post serves as explaining what I learned in my own words, while using the code from the book (meaning full credit goes to the author, while the mistakes are mine), as a way of solidification of knowledge gained.

2 Basic Stock Application

Oh, where to start? OK, first of all, we need to invoke the following command in terminal:

rust new --bin wealth_manager

This will make a package with the following structure:

│   .gitignore
│   Cargo.toml
│
└───src
        main.rs

main.rs serves as an entry point of our program, with the classical main() function.

From the start, I will isolate the modules in individual files.

2.1 stock.rs

For example, Stock struct (Python dev, read: class) will live in stocks/structs/stock.rs.

use super::utils::constructor_shout;

pub struct Stock {
    pub name: String,
    pub open_price: f32,
    pub stop_loss: f32,
    pub take_profit: f32,
    pub current_price: f32,
}

impl Stock {
    pub fn new(stock_name: &str, price: f32) -> Stock {
        constructor_shout(stock_name);
        return Stock {
            name: String::from(stock_name),
            open_price: price,
            stop_loss: 0.0,
            take_profit: 0.0,
            current_price: price,
        };
    }

    pub fn with_stop_loss(mut self, value: f32) -> Stock {
        self.stop_loss = value;
        return self;
    }

    pub fn with_take_profit(mut self, value: f32) -> Stock {
        self.take_profit = value;
        return self;
    }

    pub fn update_price(&mut self, value: f32) -> () {
        self.current_price = value;
    }
}

Let’s digest this, piece by piece. Lots of stuff is going on here.

use super::utils::constructor_shout;

To explain this, you will have to have the following files:

  • stocks/structs/utils.rs
  • stocks/structs/mod.rs

The contents of stocks/structs/utils.rs are:

pub fn constructor_shout(stock_name: &str) -> () {
    println!("the constructor for the {} is firing", stock_name);
}

So, this is just a function that notifies the user that the initialization of struct has begun. The pub keyword exposes this function to the whole directory level (not to the whole package). Argument stock_name is annotated with &str, meaning that this is a reference (&) to the string slice (str), an immutable binary which lives on stack. Also, this function returns nothing (denoted by -> ()).

Regarding stocks/structs/mod.rs, we will have to expose this file to the whole package. Python devs, think of this as your __init__.py file:

pub mod order; // to be discussed later
pub mod stock; // to be discussed later
mod utils;     // so, utils is a module ...
// ... but it will not be visible to the parent of this directory.

So, when I use:

use super::utils::constructor_shout;

\(\ldots{}\) I am saying to Rust that I need the function from parent (super), and then, when you get into parent, go to utils and get constructor_shout().

Back to `stocks/structs/stock.rs:

pub struct Stock {
    pub name: String,
    pub open_price: f32,
    pub stop_loss: f32,
    pub take_profit: f32,
    pub current_price: f32,
}

So, with struct Stock, I am basically creating a class with the fields name, open_price \(\ldots{}\), while also specifying the types of those fields. This is the way of me telling to Rust: I promise that when I give you open_price, you will get f32 float. If that is not the case, feel free to not compile the program!

So, in Python, you define your special (__init__(self)) and regular methods while defining a Class. In Rust, you can also go that route, but you don’t have to. Each new method goes into impl Stock. So, I didn’t have to put everything into one impl Stock. I could have three different impl Stocks, and there would be no difference whatsoever. But, look at this elegance, and compare it with the __init__ method in Python:

impl Stock {
    pub fn new(stock_name: &str, price: f32) -> Stock {
        constructor_shout(stock_name);
        return Stock {
            name: String::from(stock_name),
            open_price: price,
            stop_loss: 0.0,
            take_profit: 0.0,
            current_price: price,
        };
    }
}

This code does not have to be documented, everything is visible from the signature. The two init arguments are stock_name (as reference to string slice) and price (as 32-bit float). This functions return Stock object. Look at that return statement!

With new method, I cannot change stop_loss and take_profit arguments. This can be changed with the following methods (still inside impl Stock):

impl Stock {

    /// -snip- ///

    pub fn with_stop_loss(mut self, value: f32) -> Stock {
        self.stop_loss = value;
        return self;
    }

    pub fn with_take_profit(mut self, value: f32) -> Stock {
        self.take_profit = value;
        return self;
    }

    /// -snip- ///
}

Now, both struct methods expect mutable self (mut self), meaning these methods will act on initialized struct. So, chaining will be possible:

fn main() {
    // chaining is possible because each struct method returns self.
    let atlantic: Stock = Stock::new("Atlantic", 100.00)
    .with_stop_loss(74.92)
    .with_take_profit(120.00);
    // yes, this kind of syntax is possible. :)
}

These is one last method in stocks/structs/stock.rs:

impl Stock {

    // -snip- //

    pub fn update_price(&mut self, value: f32) -> () {
        self.current_price = value;
    }
}

Because of reference to mutable self (&mut self), this function will return nothing. Python devs, this is change of self object in-place. Example:

fn main() {
    // chaining is possible because each struct method returns self.
    let atlantic: Stock = Stock::new("Atlantic", 100.00)
    .with_stop_loss(74.92)
    .with_take_profit(120.00);
    // yes, this kind of syntax is possible. :)

    atlantic.update_price(150.49); // growth mindset 😂
    println!("Current share price of {} is {} HRK.", atlantic.name, atlantic.current_price);
}

This code will not compile. Why? Well, remember that Stock.update_price() requires mutable self. And I didn’t specify atlantic as mutable.

fn main() {
    // chaining is possible because each struct method returns self.
    // let mut !!!
    let mut atlantic: Stock = Stock::new("Atlantic", 100.00)
    .with_stop_loss(74.92)
    .with_take_profit(120.00);
    // yes, this kind of syntax is possible. :)

    atlantic.update_price(150.49); // growth mindset 😂
    println!("Current share price of {} is {} HRK.", atlantic.name, atlantic.current_price);
}

Output:

the constructor for the Atlantic is firing
Current share price of Atlantic is 150.05 HRK.

So, we are done with Stock struct. But, we want to define interface for our end user. For that, we need to implement additional logic.

We can buy a stock (long position) or sell a stock (short position). Buying case is self-explanatory (and selling when you are in the long position), but selling stock has reverse logic. When shorting a stock, I am borrowing stock from the broker, selling that stock, and waiting for the price to drop, so it can be bought and returned back. The difference is profit. For this type of logic, we will use Enums.

2.2 order_type.rs

So, in stocks/enums directory, we have the following files:

  • stocks/enums/order_types.rs
  • stocks/enums/mod.rs

Content of order_types.rs:

pub enum OrderType {
    Short,
    Long,
}

So, this type can only take two variants: Short and Long.

To use OrderType outside of enums directory, I have to go into mod.rs and add:

pub mod order_types;

Now, this directory is visible as a module inside of package.

2.3 order.rs

Now we can populate stocks/structs directory with the order.rs file:

use super::super::enums::order_types::OrderType;
use super::stock::Stock;
use chrono::{DateTime, Local};

pub struct Order {
    pub date: DateTime<Local>,
    pub stock: Stock,
    pub number: i32,
    pub order_type: OrderType,
}

impl Order {
    pub fn new(stock: Stock, number: i32, order_type: OrderType) -> Order {
        let today = Local::now();

        return Order {
            date: today,
            stock,
            number,
            order_type,
        };
    }

    pub fn current_value(&self) -> f32 {
        return self.stock.current_price * self.number as f32;
    }

    pub fn current_profit(&self) -> f32 {
        let current_price = self.current_value();
        let initial_price = self.stock.open_price * self.number as f32;

        match self.order_type {
            OrderType::Long => return current_price - initial_price,
            OrderType::Short => return initial_price - current_price,
        }
    }
}

Let’s dissect this.

I am importing following functions/structs:

use super::super::enums::order_types::OrderType;
use super::stock::Stock;
use chrono::{DateTime, Local};

So, with the first import statement, I am using super two times because order_type lives in stocks/enums/order_types.rs, while I am currently in stocks/structs/order.rs. So, I need to go two levels up: first to structs and then to stocks in order to access stocks/enums/order_types.rs. So, that is why I have super::super syntax (btw, very cool syntax - it can be chained as much as you want).

Similar explanation is valid for use super::stock::Stock; statement. One super is enough.

chrono is third-party package, we’re just importing DateTime and Local struct.

On to the next part of code:

pub struct Order {
    pub date: DateTime<Local>,
    pub stock: Stock,
    pub number: i32,
    pub order_type: OrderType,
}

So, our struct has 4 fields, and two of those fields have structs/enums that we defined: stock and order_type. Let’s implement more function for this struct.

Our awesome __init__ method:

impl Order {
    pub fn new(stock: Stock, number: i32, order_type: OrderType) -> Order {

        let today = Local::now(); // automatic date of order, while respecting local timezone

        return Order {
            date: today,
            stock,
            number,
            order_type,
        };
    }
}

Next, we need to implement function that calculates total value of an order:

impl Order {
    // -snip- //
    pub fn current_value(&self) -> f32 {
        return self.stock.current_price * self.number as f32;
    }
    // -snip- //
}

This method references self, and it will return float 32-bit number. Note the definition of return type in return statement.

And, now for the grand finals:

impl Order {
    pub fn current_profit(&self) -> f32 {

        let current_price = self.current_value();
        let initial_price = self.stock.open_price * self.number as f32;

        match self.order_type {
            OrderType::Long => return current_price - initial_price,
            OrderType::Short => return initial_price - current_price,
        }
    }
}

So, this function calculates total profit based on OrderType. Bonus: without if/else statements.

match self.order_type will return value based on the particular type it finds match on. If there is some other value than expected, the code will not compile!

And, let’s return to stocks/structs/utils.rs:

pub mod order;
pub mod stock;
mod utils;

In other words, out of everything we have done, we want to expose only order.rs and stock.rs to our upper level modules.

Let’s implement the interface.

2.4 mod.rs

Location: mod.rs (same folder as main.rs).

Contents:

pub mod enums;
pub mod structs;
use enums::order_types::OrderType;
use structs::order::Order;
use structs::stock::Stock;

pub fn close_order(order: Order) -> f32 {
    println!("Order for {} is being closed!", &order.stock.name);
    return order.current_profit();
}

pub fn open_order(
    number: i32,
    order_type: OrderType,
    stock_name: &str,
    open_price: f32,
    stop_loss: Option<f32>,
    take_profit: Option<f32>,
) -> Order {
    println!("Order for {} is being made!", &stock_name);

    let mut stock: Stock = Stock::new(stock_name, open_price);

    match stop_loss {
        Some(value) => stock = stock.with_stop_loss(value),
        None => {}
    }

    match take_profit {
        Some(value) => stock = stock.with_take_profit(value),
        None => {}
    }

    return Order::new(stock, number, order_type);
}

Self-explanatory.
pub mod enums;
pub mod structs;
use enums::order_types::OrderType;
use structs::order::Order;
use structs::stock::Stock;

So, we’re using enums and structs, particularly OrderType, Stock and Order, which are visible precisely because they have pub keyword and because enums directory has mod.rs file which exposes it’s pub contents to parent.

pub fn close_order(order: Order) -> f32 {
    println!("Order for {} is being closed!", &order.stock.name);
    return order.current_profit();
}

This function is not part of any struct (for Python devs, reminder: class). This is isolated function that closes order. Remember that I can close order by selling stock (if I am in long position) and buy stock (if I am in short position). Since we’ve made promise to close_order() that it will get and Order object, I can precisely return precise amount of profits (or loss) based on the OrderType (order_type) that Order has for argument during __init__ phase.

And if I want to buy a stock as an end-user, I don’t have to meddle with Stock object:

pub fn open_order(
    number: i32,
    order_type: OrderType,
    stock_name: &str,
    open_price: f32,
    stop_loss: Option<f32>,
    take_profit: Option<f32>,
) -> Order {
    println!("Order for {} is being made!", &stock_name);

    let mut stock: Stock = Stock::new(stock_name, open_price);

    match stop_loss {
        Some(value) => stock = stock.with_stop_loss(value),
        None => {}
    }

    match take_profit {
        Some(value) => stock = stock.with_take_profit(value),
        None => {}
    }

    return Order::new(stock, number, order_type);
}

This function will initialize Stock object for me, and I will get back appropriate Order object, where I, as an end-user, only care about Order.current_value() and Order.current_profit().

Let’s implement this in main.rs.

2.5 Implementation in main.rs

Let’s get away from Croatian stocks, and buy some blue-chip tech stocks.

mod stocks;

use stocks::enums::order_types::OrderType;
use stocks::structs::order::Order;
use stocks::structs::stock::Stock;
use stocks::{close_order, open_order};

fn main() {
    
    // testing of Stock struct
    let stock = Stock::new("AAPL", 155.74);
    println!("Here is the stock name: {}", stock.name);
    println!("Here is the stock price: {}", stock.current_price);

    // Experience of an end user
    let mut new_order = open_order(324.77, OrderType::Long, "AAPL", 155.74, None, None);

    println!("\nThe current value is {}", &new_order.current_value());
    println!("The current profit is {}", &new_order.current_profit());

    new_order.stock.update_price(100.0); // bear market

    println!("\nThe current value is {}", &new_order.current_value());
    println!("The current profit is {}", &new_order.current_profit());

    new_order.stock.update_price(200.00); // bull market

    println!("\nThe current value is {}", &new_order.current_value());
    println!("The current profit is {}", &new_order.current_profit());

    // profit is great, time to close the order:
    let profit = close_order(new_order);

    println!("\nWe made {profit} USD");
}

Output:

# Testing of Stock struct
the constructor for the AAPL is firing
Here is the stock name: AAPL
Here is the stock price: 155.74

# order of AAPL
Order for AAPL is being made!
the constructor for the AAPL is firing

The current value is 50615.5
The current profit is 0

# bear market
The current value is 32500
The current profit is -18115.5

# let's take the profits
The current value is 65000
The current profit is 14384.5
Order for AAPL is being closed!

3 Conclusion

Meme aside, during the creation of this package, I was really amazed with Rust’s compiler. Memes are true, it actually is very helpful in understanding where you messed up, and not only that, it gives you hints how to solve the problem. As I remember from my chess phase at college, Hikaru Nakamura didn’t read chess theory, but constantly played chess games against AI until he understood the patterns. If this is true, that’s amazing.

In a way, I think Rust gives that kind of opportunity/training to any programmer, regardless of experience. You have great mentor by your side that will instill into you good coding practices from the start.

Anyways, that’s all for now, back to the book. 😊

4 Resources

  1. Maxwell Flitton - Speed Up Your Python with Rust: Optimize Python performance by creating Python pip modules in Rust with PyO3.
  2. Steve Klabnik and Carol Nichols, with contributions from the Rust Community - The Rust Programming Language.