Home Programming Speed Up Your Python with Rust

Speed Up Your Python with Rust

By Maxwell Flitton
books-svg-icon Book
eBook $35.99 $24.99
Print $43.99 $25.99
Subscription $15.99 $10 p/m for three months
$10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
BUY NOW $10 p/m for first 3 months. $15.99 p/m after that. Cancel Anytime!
eBook $35.99 $24.99
Print $43.99 $25.99
Subscription $15.99 $10 p/m for three months
What do you get with a Packt Subscription?
This book & 7000+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook + Subscription?
Download this book in EPUB and PDF formats, plus a monthly download credit
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with a Packt Subscription?
This book & 6500+ ebooks & video courses on 1000+ technologies
60+ curated reading lists for various learning paths
50+ new titles added every month on new and emerging tech
Early Access to eBooks as they are being written
Personalised content suggestions
Customised display settings for better reading experience
50+ new titles added every month on new and emerging tech
Playlists, Notes and Bookmarks to easily manage your learning
Mobile App with offline access
What do you get with eBook?
Download this book in EPUB and PDF formats
Access this title in our online reader
DRM FREE - Read whenever, wherever and however you want
Online reader with customised display settings for better reading experience
What do you get with video?
Download this video in MP4 format
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with video?
Stream this video
Access this title in our online reader
DRM FREE - Watch whenever, wherever and however you want
Online reader with customised display settings for better learning experience
What do you get with Audiobook?
Download a zip folder consisting of audio files (in MP3 Format) along with supplementary PDF
What do you get with Exam Trainer?
Flashcards, Mock exams, Exam Tips, Practice Questions
Access these resources with our interactive certification platform
Mobile compatible-Practice whenever, wherever, however you want
  1. Free Chapter
    Chapter 1: An Introduction to Rust from a Python Perspective
About this book
Python has made software development easier, but it falls short in several areas including memory management that lead to poor performance and security. Rust, on the other hand, provides memory safety without using a garbage collector, which means that with its low memory footprint, you can build high-performant and secure apps relatively easily. However, rewriting everything in Rust can be expensive and risky as there might not be package support in Rust for the problem being solved. This is where Python bindings and pip come in. This book will help you, as a Python developer, to start using Rust in your Python projects without having to manage a separate Rust server or application. Seeing as you'll already understand concepts like functions and loops, this book covers the quirks of Rust such as memory management to code Rust in a productive and structured manner. You'll explore the PyO3 crate to fuse Rust code with Python, learn how to package your fused Rust code in a pip package, and then deploy a Python Flask application in Docker that uses a private Rust pip module. Finally, you'll get to grips with advanced Rust binding topics such as inspecting Python objects and modules in Rust. By the end of this Rust book, you'll be able to develop safe and high-performant applications with better concurrency support.
Publication date:
January 2022
Publisher
Packt
Pages
384
ISBN
9781801811446

 

Chapter 1: An Introduction to Rust from a Python Perspective

Due to its speed and safety, it is no surprise that Rust is the new language gaining in popularity. However, with success comes criticism. Despite Rust's popularity as an impressive language, it has also gained the label of being hard to learn, an idea which isn't quite grounded in reality.

In this chapter, we will cover all of Rust's quirks that will be new to a Python developer. If Python is your main language, concepts such as basic memory management and typing can initially slow down your ability to quickly write productive Rust code due to the compiler failing to compile the code. However, this can quickly be overcome by learning the rules around Rust features, such as variable ownership, lifetimes, and so on, as Rust is a memory-safe language. Consequently, we must keep track of our variables as they usually get deleted instantly when they go out of scope. If this does not make sense yet, don't worry; we will cover this concept in the Keeping track of scopes and lifetimes section.

In this chapter, we will also be covering the basics of syntax, while you will be setting up a Rust environment on your own computer in the next chapter. Do not worry though, you can code all the examples in this chapter on the free online Rust playground.

In particular, we will cover the following topics in this chapter:

  • Understanding the differences between Python and Rust
  • Understanding variable ownership
  • Keeping track of scopes and lifetimes
  • Building structs as opposed to objects
  • Metaprogramming with macros instead of decorators
 

Technical requirements

As this is just an introduction, all the Python examples in the chapter can be implemented with a free online Python interpreter such as https://replit.com/languages/python3.

The same goes for all the Rust examples. These can be implemented using the free online Rust playground found at https://play.rust-lang.org/.

The code covered in the chapter can be found at https://github.com/PacktPublishing/Speed-up-your-Python-with-Rust/tree/main/chapter_one.

 

Understanding the differences between Python and Rust

Rust can sometimes be described as a systems language. As a result, it can sometimes be labeled by software engineers in a way that is similar to C++: fast, hard to learn, dangerous, and time-consuming to code in. As a result, most of you mainly working in dynamic languages such as Python could be put off. However, Rust is memory-safe, efficient, and productive. Once we have gotten over some of the quirks that Rust introduces, nothing is holding you back from exploiting Rust's advantages by using it to write fast, safe, and efficient code. Seeing as there are so many advantages to Rust, we will explore them in the next section.

Why fuse Python with Rust?

When it comes to picking a language, there is usually a trade-off between resources, speed, and development time. Dynamic languages such as Python became popular as computing power increased. We were able to use the extra resources we had to manage our memory with garbage collectors. As a result, developing software became easier, quicker, and safer. As we will cover later in the Keeping track of scopes and lifetimes section, poor memory management can lead to some security flaws. The exponential increase in computing power over the years is known as Moore's Law. However, this is not continuing to hold and in 2019, Nvidia's CEO Jensen Huang suggested that as chip components get closer to the size of individual atoms, it has gotten harder to keep up with the pace of Moore's Law, thus declaring it dead (https://www.cnet.com/news/moores-law-is-dead-nvidias-ceo-jensen-huang-says-at-ces-2019/).

However, with the rise of big data, our need to pick up faster languages to satisfy our needs is increasing. This is where languages such as Golang and Rust enter. These languages are memory-safe, yet they compile and have significant speed increases. What makes Rust even more unique is that it has managed to achieve memory safety without garbage collection. To appreciate this, let's briefly describe garbage collection: this is where the program temporarily stops, checks all the variables to see which ones are no longer being used, and deletes those that are not. Considering that Rust does not have to do this, it is a significant advantage as Rust does not have to keep stopping to clean up the variables. This was demonstrated in Discord's 2020 blog post Why Discord is switching from Go to Rust: https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f#:~:text=The%20service%20we%20switched%20from,is%20in%20the%20hot%20path. In this post, we can see that Golang just could not keep up with Rust, as demonstrated in the graph they presented:

Figure 1.1 – Golang is spiky and Rust is the flat line below Golang

Figure 1.1 – Golang is spiky and Rust is the flat line below Golang (image source: https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f#:~:text=The%20service%20we%20switched%20from,is%20in%20the%20hot%20path)

The comments on the post were full of people complaining that Discord used an out-of-date version of Golang. Discord responded to this by stating that they tried a range of Golang versions, and they all had similar results. With this, it makes sense to get the best of both worlds without much compromise. We can use Python for prototyping and complex logic. The extensive range of third-party libraries that Python has combined with the flexible object-oriented programming it supports make it an ideal language for solving real-world problems. However, it's slow and is not efficient with the use of resources. This is where we reach for Rust.

Rust is a bit more restrictive in the way we can lay out and structure the code; however, it's fast, safe, and efficient when implementing multithreading. Combining these two languages enables a Python developer to have a powerful tool in their belt that their Python code can use when needed. The time investment needed to learn and fuse Rust is low. All we must do is package Rust and install it in our Python system using pip and understand a few quirks that Rust has that are different from Python. We can start this journey by looking at how Rust handles strings in the next section. However, before we explore strings, we have to first understand how Rust is run compared to Python.

If you have built a web app in Python using Flask, you will have seen multiple tutorials sporting the following code:

from flask import Flask
app = Flask(__name__)
@app.route("/")
def home():
    return "Hello, World!"
    
if __name__ == "__main__":
    app.run(debug=True)

What we must note here is the last two lines of the code. Everything above that defines a basic Flask web app and a route. However, the running of the app in the last two lines only executes if the Python interpreter is directly running the file. This means that other Python files can import the Flask app from this file without running it. This is referred to by many as an entry point.

You import everything you need in this file, and for the application to run, we get our interpreter to run this script. We can nest any code under the if __name__ == "__main__": line of code. It will not run unless the file is directly hit by the Python interpreter. Rust has a similar concept. However, this is more essential, as opposed to Python that just has it as a nice-to-have feature. In the Rust playground (see the Technical requirements section), we can type in the following code if it is not there already:

fn main() {
    println!("hello world");
}

This is the entry point. The Rust program gets compiled, and then runs the main function. If whatever you've coded is not accessed by the main function, it will never run. Here, we are already getting a sense of the safety enforced by Rust. We will see more of this throughout the book.

Now that we have our program running, we can move on to understanding the difference between Rust and Python when it comes to strings.

Passing strings in Rust

In Python, strings are flexible. We can pretty much do what we want with them. While technically, Python strings cannot be changed under the hood, in Python syntax, we can chop and change them, pass them anywhere, and convert them into integers or floats (if permitted) without having to think too much about it. We can do all of this with Rust too. However, we must plan beforehand what we are going to do. To demonstrate this, we can dive right in by making our own print function and calling it, as seen in the following code:

fn print(input: str) {
    println!("{}", input);
}
fn main() {
    print("hello world");
}

In Python, a similar program would work. However, when we run it in the Rust playground, we get the following error:

error[E0277]: the size for values of type 'str' cannot be known at compilation time

This is because we cannot specify what the maximum size is. We don't get this in Python; therefore, we must take a step back and understand how variables are assigned in memory. When the code compiles, it allocates memory for different variables in the stack. When the code runs, it stores data in the heap. Strings can be various sizes so we cannot be sure at compile time how much memory we can allocate to the input parameter of our function when compiling. What we are passing in is a string slice. We can remedy this by passing in a string and converting our string literal to a string before passing it into our function as seen here:

fn print(input: String) {
    println!("{}", input);
}
fn main() {
    let string_literal = "hello world";
    print(string_literal.to_string());
}

Here, we can see that we have used the to_string() function to convert our string literal into a string. To understand why String is accepted, we need to understand what a string is.

A string is a type of wrapper implemented as a vector of bytes. This vector holds a reference to a string slice in the heap memory. It then holds the amount of data available to the pointer, and the length of the string literal. For instance, if we have a string of the string literal one, it can be denoted by the following diagram:

Figure 1.2 – String relationship to str

Figure 1.2 – String relationship to str

Considering this, we can understand why we can guarantee the size of String when we pass it into our function. It will always be a pointer to the string literal with some meta-information about the string literal. If we can just make a reference to the string literal, we can pass this into our function as it is just a reference and we can therefore guarantee that the size of the reference will stay the same. This can be done by borrowing using the & operator as shown in the following code:

fn print(input_string: &str) {
    println!("{}", input_string);
}
fn main() {
    let test_string = &"Hello, World!";
    print(test_string);
}

We will cover the concept of borrowing later in the chapter but, for now, we understand that, unlike Python, we must guarantee the size of the variable being passed into a function. We can use borrowing and wrappers such as strings to handle this. It may not come as a surprise, but this does not just stop at strings. Considering this, we can move on to the next section to understand the differences between Python and Rust when it comes to floats and integers.

Sizing up floats and integers in Rust

Like strings, Python manages floats and integers with ease and simplicity. We can pretty much do whatever we want with them. For instance, the following Python code will result in 6.5:

result = 1 + 2.2
result = result + 3.3

However, there is a problem when we try to just execute the first line in Rust with the following line of Rust code:

let result = 1 + 2.2;

It results in an error telling us that a float cannot be added to an integer. This error highlights one of the pain points that Python developers go through when learning Rust, as Rust enforces typing aggressively by refusing to compile if typing is not present and consistent. However, while this is an initial pain, aggressive typing does help in the long run as it maintains safety.

Type annotation in Python is gaining popularity. This is where the type of the variable is declared for parameters of functions or variables declared, enabling some editors to highlight when the types are inconsistent. The same happens in JavaScript with TypeScript. We can replicate the Python code at the start of this section with the following Rust code:

let mut result = 1.0 + 2.2;
result = result + 3.3;

It has to be noted that the result variable must be declared as a mutable variable with the mut notation. Mutable means that the variable can be changed. This is because Rust automatically assigns all variables as immutable unless we use the mut notation.

Now that we have seen the effects of types and mutability, we should really explore integers and floats. Rust has two types of integers: signed integers, which are denoted by i, and unsigned integers, denoted by u. Unsigned integers only house positive numbers, whereas signed integers house positive and negative integers. This does not just stop here. In Rust, we can also denote the size of the integer that is allowed. This can be calculated by using binary. Now, understanding how to use binary notation to describe numbers in detail is not really needed. However, understanding the simple rule that the size can be calculated by raising two to the power of the number of bits can give us an understanding of how big an integer is allowed to be. We can calculate all the integer sizes that we can utilize in Rust with the following table:

Table 1.1 – Size of integer types

Table 1.1 – Size of integer types

As we can see, we can get to very high numbers here. However, it is not the best idea to assign all variables and parameters as u128 integers. This is because the compiler will set aside this amount of memory each time when compiling. This is not very efficient considering that it's unlikely that we will be using such large numbers. It must be noted that the changes in each jump are so large it is pointless graphing it. Each jump in bits completely overshadows all the others, resulting in a flat line along the x axis and a huge spike at the last graphed number of bits. However, we also must be sure that our assignment is not too small. We can demonstrate this with the Rust code as follows:

let number: u8 = 255;
let breaking_number: u8 = 256;  

Our compiler will be OK with the number variable. However, it will throw the error shown next when assigning the breaking_number variable:

literal '256' does not fit into the type 'u8' whose range 
is '0..=255'

This is because there are 256 integers between 0 -> 255, as we include 0. We can change our unsigned integer to a signed one with the following line of Rust code:

let number: i8 = 255;

This gives us the following error:

literal '255' does not fit into the type 'i8' whose range 
is '-128..=127'

In this error, we are reminded that the bits are are allocated memory space. Therefore, an i8 integer must accommodate positive and negative integers within the same number of bits. As a result, we can only support a magnitude that is half of the integer of an unsigned integer.

When it comes to floats, our choices are more limited. Here, Rust accommodates both f32 and f64 floating points. Declaring these floating-point variables requires the same syntax as integers:

let float: f32 = 20.6;

It must be noted that we can also annotate numbers with suffixes, as shown in the following code:

let x = 1u8;

Here, x has a value of 1 with the type of u8. Now that we have covered floats and integers, we can use vectors and arrays to store them.

Managing data in Rust's vectors and arrays

With Python, we have lists. We can stuff anything we want into these lists with the append function and these lists are, by default, mutable. Python tuples are technically not lists, but we can treat them as immutable arrays. With Rust, we have arrays and vectors. Arrays are the most basic of the two. Defining and looping through an array is straightforward in Rust, as we can see in the following code:

let array: [i32; 3] = [1, 2, 3];
println!("array has {} elements", array.len());
for i in array.iter() {
    println!("{}", i);
}

If we try and append another integer onto our array with the push function, we will not be able to even if the array is mutable. If we add a fourth element to our array definition that is not an integer, the program will refuse to compile as all of the elements in the array have to be the same. However, this is not entirely true.

Later in this chapter, we will cover structs. In Python, the closest comparison to objects is structs as they have their own attributes and functions. Structs can also have traits, which we will also discuss later. In terms of Python, the closest comparison to traits is mixins. Therefore, a range of structs can be housed in an array if they all have the same trait in common. When looping through the array, the compiler will only allow us to execute functions from that trait as this is all we can ensure will be consistent throughout the array.

The same rules in terms of type or trait consistency also apply to vectors. However, vectors place their memory on the heap and are expandable. Like everything in Rust, they are, by default, immutable. However, applying the mut tag will enable us to add and manipulate the vector. In the following code, we define a vector, print the length of the vector, append another element to the vector, and then loop through the vector printing all elements:

let mut str_vector: Vec<&str> = vec!["one", "two", \
  "three"];
println!("{}", str_vector.len());
str_vector.push("four");
for i in str_vector.iter() {
    println!("{}", i);
}

This gives us the following output:

3
one
two
three
four

We can see that our append worked.

Considering the rules about consistency, vectors and arrays might seem a little restrictive to a Python developer. However, if they are, sit back and ask yourself why. Why would you want to put in a range of elements that do not have any consistency? Although Python allows you to do this, how could you loop through a list with inconsistent elements and confidently perform operations on them without crashing the program?

With this in mind, we are starting to see the benefits and safety behind this restrictive typing system. There are some ways in which we can put in different elements that are not structs bound by the same trait. Considering this, we will explore how we can store and access our varied data elements via hashmaps in Rust in the next section.

Replacing dictionaries with hashmaps

Hashmaps in Rust are essentially dictionaries in Python. However, unlike our previous vectors and arrays, we want to have a range of different data types housed in a hashmap (although we can also do this with vectors and arrays). To achieve this, we can use Enums. Enums are, well, Enums, and we have the exact same concept in Python. However, instead of it being an Enum, we merely have a Python object that inherits the Enum object as seen in the following code:

from enum import Enum 
class Animal(Enum):
    STRING = "string"
    INT = "int"

Here, we can use the Enum to save us from using raw strings in our Python code when picking a particular category. With a code editor known as an IDE, this is very useful, but it's understandable if a Python developer has never used them as they are not enforced anywhere. Not using them makes the code more prone to mistakes and harder to maintain when categories change and so on, but there is nothing in Python stopping the developer from just using a raw string to describe an option. In Rust, we are going to want our hashmap to accept strings and integers. To do this, we are going to have to carry out the following steps:

  1. Create an Enum to handle multiple data types.
  2. Create a new hashmap and insert values belonging to the Enum we created in step 1.
  3. Test the data consistency by looping through the hashmap and match all possible outcomes.
  4. Build a function that processes data extracted from the hashmap.
  5. Use the function to process outcomes from getting a value from the hashmap.

Therefore, we are going to create an Enum that houses this using the following code:

enum Value {
    Str(&'static str),
    Int(i32),
}

Here, we can see that we have introduced the statement 'static. This denotes a lifetime and basically states that the reference remains for the rest of the program's lifetime. We will cover lifetimes in the Keeping track of scopes and lifetimes section.

Now that we have defined our Enum, we can build our own mutable hashmap and insert an integer and a string into it with the following code:

use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("one", Value::Str("1"));
map.insert("two", Value::Int(2));

Now that our hashmap is housing a single type that houses the two types we defined, we must handle them.

Remember, Rust has strong typing. Unlike Python, Rust will not allow us to compile unsafe code (Rust can compile in an unsafe context but this is not default behavior). We must handle every possible outcome, otherwise the compiler will refuse to compile. We can do this with a match statement as seen in the following code:

for (_key, value) in &map {
    match value {
        Value::Str(inside_value) => {
            println!("the following value is an str: {}", \ 
                inside_value);
        }
        Value::Int(inside_value) => {
            println!("the following value is an int: {}", \ 
                inside_value);
        }
    }
}

In this code sample, we have looped through a borrowed reference to the hashmap using &. Again, we will cover borrowing later on in the Understanding variable ownership section. We prefix the key with a _. This is telling the compiler that we are not going to use the key. We don't have to do this as the compiler will still compile the code; however, it will complain by issuing a warning. The value that we are retrieving from the hashmap is our Value Enum. In this match statement, we can match the field of our Enum, and unwrap and access the inside value that we denote as inside_value, printing it to the console.

Running the code gives us the printout to the terminal as follows:

the following value is an int: 2
the following value is an str: 1

It must be noted that Rust is not going to let anything slip by the compiler. If we remove the match for our Int field for our Enum, then the compiler will throw the error seen here:

18 |           match value {
   |           ^^^^^ pattern '&Int(_)' not covered
   |
   = help: ensure that all possible cases are being 
     handled, 
   possibly by adding wildcards or more match arms
   = note: the matched value is of type '&Value'

This is because we have to handle every single possible outcome. Because we have been explicit that only values that can be housed in our Enum can be inserted into the hashmap, we know that there are only two possible types that can be extracted from our hashmap. We have nearly covered enough about hashmaps to use them effectively in Rust programs. One last concept that we must cover is the Enum called Option.

Considering that we have arrays and vectors, we will not be using our hashmaps primarily for looping through outcomes. Instead, we will be retrieving values from them when we need them. Like in Python, the hashmap has a get function. In Python, if the key that is being searched is not in the dictionary, then the get function will return None. It is then left to the developer to decide what to do with it. However, in Rust, the hashmap will return a Some or None. To demonstrate this, let's try to get a value belonging to a key that we know is not there:

  1. Start by running the following code:
    let outcome: Option<&Value> = map.get("test");
    println!("outcome passed");
    let another_outcome: &Value = \
        map.get("test").unwrap();
    println!("another_outcome passed");

    Here, we can see that we can access the reference to the Value Enum wrapped in Option with the get function. We then directly access the reference to the Value Enum using the unwrap function.

  2. However, we know that the test key is not in the hashmap. Because of this, the unwrap function will cause the program to crash, as seen in the following output from the previous code:
    thread 'main' panicked at 'called 'Option::unwrap()' 
    on a 'None' value', src/main.rs:32:51

    We can see that the simple get function did not crash the program. However, we didn't manage to get the string "another_outcome passed" to print out to the console. We can handle this with a match statement.

    However, this is going to be a match statement within a match statement.

  3. In order to reduce the complexity, we should explore Rust functions to process our value Enum. This can be done with the following code:
    fn process_enum(value: &Value) -> () {
        match value {
            Value::Str(inside_value) => {
                println!("the following value is an str: \
                  {}", inside_value);
            }
            Value::Int(inside_value) => {
                println!("the following value is an int: \
                  {}", inside_value);
            }
        }
    }

    The function does not really give us any new logic to explore. The -> () expression is merely stating that the function is not returning anything.

  4. If we are going to return a string, for instance, the expression would be -> String. We do not need the -> () expression; however, it can be helpful for developers to quickly understand what's going on with the function. We can then use this function to process the outcome from our get function with the following code:
    match map.get("test") {
        Some(inside_value) => {
            process_enum(inside_value);
        }
        None => {
            println!("there is no value");
        }
    }

We now know enough to utilize hashmaps in our programs. However, we must notice that we have not really handled errors; we have either printed out that nothing was found or let the unwrap function just result in an error. Considering this, we will move on to the next section on handling errors in Rust.

Error handling in Rust

Handling errors in Python is straightforward. We have a try block that houses an except block underneath. In Rust, we have a Result wrapper. This works in the same way as an Option. However, instead of having Some or None, we have Ok or Err.

To demonstrate this, we can build on the hashmap that was defined in the previous section. We accept Option from a get function applied to the hashmap. Our function will check to see whether the integer retrieved from the hashmap is above a threshold. If it's above the threshold, we will return a true value. If not, then it is false.

The problem is that there might not be a value in Option. We also know that the Value Enum might not be an integer. If any of this is the case, we should return an error. If not, we return a Boolean. This function can be seen here:

fn check_int_above_threshold(threshold: i32, 
    get_result: Option<&Value>) -> Result<bool, &'static \
      str> {
    match get_result {
      Some(inside_value) => {
        match inside_value {
          Value::Str(_) => return Err(
            "str value was supplied as opposed to \
              an int which is needed"),
                Value::Int(int_value) => {
                    if int_value > &threshold {
                        return Ok(true)
                    }
                    return Ok(false)
                } 
            }
        }
        None => return Err("no value was supplied to be \
          checked")
    }
}

Here, we can see that the None result from Option instantly returns an error with a helpful message as to why we are returning an error. With the Some value, we utilize another match statement to return an error with a helpful message that we cannot supply a string to check the threshold if the Value is a string. It must be noted that Value::Str(_) has a _ in it. This means that we do not care what the value is because we are not going to use it. In the final part, we check to see whether the integer is above the threshold returning Ok values that are either true or false. We implement this function with the following code:

let result: Option<&Value> = map.get("two");
let above_threshold: bool = check_int_above_threshold(1, \
    result).unwrap();
println!("it is {} that the threshold is breached", \
    above_threshold);

This gives us the following output in the terminal:

it is true that the threshold is breached

If we up the first parameter in our check_int_above_threshold function to 3, we get the following output:

it is false that the threshold is breached

If we change the key in map.get to three, we get the following terminal output:

thread 'main' panicked at 'called 'Result::unwrap()' 
on an 'Err' value: "no value was supplied to be checked"'

If we change the key in map.get to one, we get the following terminal output:

thread 'main' panicked at 'called 'Result::unwrap()' on 
an 'Err' value: "str value was supplied as opposed to an 
int

We can add extra signposting to the unwrap with the expect function. This function unwraps the result and adds an extra message to the printout if there is an error. With the following implementation, the message "an error happened" will be added to the error message:

let second_result: Option<&Value> = map.get("one");
let second_threshold: bool = check_int_above_threshold(1, \
    second_result).expect("an error happened");

We can also directly throw an error if needed with the following code:

panic!("throwing some error");

We can also check to see whether the result is an error by using the is_err function as seen here:

result.is_err() 

This returns a bool, enabling us to alter the direction of our program if we come across an error. As we can see, Rust gives us a range of ways in which we can throw and manage errors.

We can now handle enough of Rust's quirks to write basic scripts. However, if the program gets a little more complicated, we fall into other pitfalls such as variable ownership and lifetimes. In the next section, we cover the basics of variable ownership so we can continue to use our variables throughout a range of functions and structs.

 

Understanding variable ownership

As we pointed out in the introduction discussing why we should use Rust, Rust doesn't have a garbage collector; however, it is still memory-safe. We do this to keep the resources low and the speed high. However, how do we achieve memory safety without a garbage collector? Rust achieves this by enforcing some strict rules around variable ownership.

Like typing, these rules are enforced when the code is being compiled. Any violation of these rules will stop the compilation process. This can lead to a lot of initial frustration for Python developers, as Python developers like to use their variables as and when they want. If they pass a variable into a function, they also expect that variable to still be able to be mutated outside the function if they want. This can lead to issues when implementing concurrent executions. Python also allows this by running expensive processes under the hood to enable the multiple references with cleanup mechanisms when the variable is no longer referenced.

As a result, this mismatch in coding style gives Rust the false label of having a steep learning curve. If we learn the rules, we only must rethink our code a little, as the helpful compiler enables us to adhere to them easily. You'll also be surprised how this approach is not as restrictive as it sounds. Rust's compile-time checking is done to protect against the following memory errors:

  • Use after frees: This is where memory is accessed once it has been freed, which can cause crashes. It can also allow hackers to execute code via this memory address.
  • Dangling pointers: This is where a reference points to a memory address that no longer houses the data that the pointer was referencing. Essentially, this pointer now points to null or random data.
  • Double frees: This is where allocated memory is freed, and then freed again. This can cause the program to crash and increases the risk of sensitive data being revealed. This also enables a hacker to execute arbitrary code.
  • Segmentation faults: This is where the program tries to access the memory it's not allowed to access.
  • Buffer overrun: An example of this is reading off the end of an array. This can cause the program to crash.

Rust manages to protect against these errors by enforcing the following rules:

  • Values are owned by the variables assigned to them.
  • As soon as the variable goes out of scope, it is deallocated from the memory it is occupying.
  • Values can be used by other variables, if we adhere to the conventions around copying, moving, immutable borrowing, and mutable borrowing.

To really feel comfortable navigating these rules in code, we will explore copying, moving, immutable borrowing, and mutable borrowing in more detail.

Copy

This is where the value is copied. Once it has been copied, the new variable owns the value, and the existing variable also owns its own value:

Figure 1.3 – Variable Copy path

Figure 1.3 – Variable Copy path

As we can see with the pathway diagram in Figure 1.3, we can continue to use both variables. If the variable has a Copy trait, the variable will automatically copy the value. This can be achieved by the following code:

let one: i8 = 10;
let two: i8 = one + 5;
println!("{}", one);
println!("{}", two);

The fact that we can print out both the one and two variables means we know that one has been copied and the value of this copy has been utilized by two. Copy is the simplest reference operation; however, if the variable being copied does not have a Copy trait, then the variable must be moved. To understand this, we will now explore moving as a concept.

Move

This is where the value is moved from one variable to another. However, unlike Copy, the original variable no longer owns the value:

Figure 1.4 – Variable Move path

Figure 1.4 – Variable Move path

Looking at the path diagram in Figure 1.4, we can see that one can no longer be used as it's been moved to two. We mentioned in the Copy section that if the variable does not have the Copy trait, then the variable is moved. In the following code, we show this by doing what we did in the Copy section but using String as this does not have a Copy trait:

let one: String = String::from("one");
let two: String = one + " two";
println!("{}", two);
println!("{}", one);

Running this gives the following error:

let one: String = String::from("one");
    --- move occurs because 'one' has type 
    'String', which does not implement the 
    'Copy' trait
let two: String = one + " two";
        ------------ 'one' moved due to usage in operator
println!("{}", two);
println!("{}", one);
        ^^^ value borrowed here after move

This is really where the compiler shines. It tells us that the string does not implement the Copy trait. It then shows us where the move occurs. It is no surprise that many developers praise the Rust compiler. We can get round this by using the to_owned function with the following code:

let two: String = one.to_owned() + " two";

It is understandable to wonder why Strings do not have the Copy trait. This is because the string is a pointer to a string slice. Copying actually means copying bits. Considering this, if we were to copy strings, we would have multiple unconstrained pointers to the same string literal data, which would be dangerous. Scope also plays a role when it comes to moving variables. In order to see how scope forces movement, we need to explore immutable borrows in the next section.

Immutable borrow

This is where one variable can reference the value of another variable. If the variable that is borrowing the value falls out of scope, the value is not deallocated from memory as the variable borrowing the value does not have ownership:

Figure 1.5 – Immutable borrow path

Figure 1.5 – Immutable borrow path

We can see with the path diagram in Figure 1.5 that two borrows the value from one. When this is happening, one is kind of locked. We can still copy and borrow one; however, we cannot do a mutable borrow or move while two is still borrowing the value. This is because if we have mutable and immutable borrows of the same variable, the data of that variable could change through the mutable borrow causing an inconsistency. Considering this, we can see that we can have multiple immutable borrows at one time while only having one mutable borrow at any one time. Once two is finished, we can do anything we want to one again. To demonstrate this, we can go back to creating our own print function with the following code:

fn print(input_string: String) -> () {
    println!("{}", input_string);
}

With this, we create a string and pass it through our print function. We then try and print the string again, as seen in the following code:

let one: String = String::from("one");
print(one);
println!("{}", one);

If we try and run this, we will get an error stating that one was moved into our print function and therefore cannot be used in println!. We can solve this by merely accepting a borrow of a string using & in our function, as denoted in the following code:

fn print(input_string: &String) -> () {
    println!("{}", input_string);
}

Now we can pass a borrowed reference into our print function. After this, we can still access the | variable, as seen in the following code:

let one: String = String::from("one");
print(&one);
let two: String = one + " two";
println!("{}", two);

Borrows are safe and useful. As our programs grow, immutable borrows are safe ways to pass variables through to other functions in other files. We are nearly at the end of our journey toward understanding the rules. The only concept left that we must explore is mutable borrows.

Mutable borrow

This is where another variable can reference and write the value of another variable. If the variable that is borrowing the value falls out of scope, the value is not deallocated from memory as the variable borrowing the value does not have ownership. Essentially, a mutable borrow has the same path as an immutable borrow. The only difference is that while the value is being borrowed, the original variable cannot be used at all. It will be completely locked down as the value might be altered when being borrowed. The mutable borrow can be moved into another scope like a function, but cannot be copied as we cannot have multiple mutable references, as stated in the previous section.

Considering all that we have covered on borrowing, we can see a certain theme. We can see that scopes play a big role in implementing the rules that we have covered. If the concept of scopes is unclear, passing a variable into a function is changing scope as a function is its own scope. To fully appreciate this, we need to move on to exploring scopes and lifetimes.

 

Keeping track of scopes and lifetimes

In Python, we do have the concept of scope. It is generally enforced in functions. For instance, we can call the Python function defined here:

def add_and_square(one: int, two: int) -> int:
    total: int = one + two
    return total * total

In this case, we can access the return variable. However, we will not be able to access the total variable. Outside of this, most of the variables are accessible throughout the program. With Rust, it is different. Like typing, Rust is aggressive with scopes. Once a variable is passed into a scope, it is deleted when the scope is finished. Rust manages to maintain memory safety without garbage collection with the borrowing rules. Rust deletes its variables without garbage collection by wiping all variables out of scope. It can also define scopes with curly brackets. A classic way of demonstrating scopes can be done by the following code:

fn main() {
    let one: String = String::from("one");
    // start of the inner-scope
    { 
        println!("{}", &one);
        let two: String = String::from("two");
    } 
    // end of the inner-scope
    println!("{}", one);
    println!("{}", two);
}

If we try and run this code, we get the error code defined here:

println!("{}", two);
        ^^^ not found in this scope

We can see that the variable one can be accessed in the inner-scope as it was defined outside the outer-scope. However, the variable two is defined in the inner-scope. Once the inner-scope is finished, we can see by the error that we cannot access the variable two outside the inner-scope. We must remember that the scope of functions is a little stronger. From revising borrowing rules, we know that when we move a variable into the scope of a function, it cannot be accessed outside of the scope of the function if the variable is not borrowed as it is moved. However, we can still alter a variable inside another scope like another function, and still then access the changed variable. To do this, we must do a mutable borrow, and then must dereference (using *) the borrowed mutable variable, alter the variable, and then access the altered variable outside the function, as we can see with the following code:

fn alter_number(number: &mut i8) {
    *number += 1
}
fn print_number(number: i8) {
    println!("print function scope: {}", number);
}
    
fn main() {
    let mut one: i8 = 1;
    print_number(one);
    alter_number(&mut one);
    println!("main scope: {}", one);
}

This gives us the following output:

print function scope: 1
main scope: 2

With this, we can see that that if we are comfortable with our borrowing, we can be flexible and safe with our variables. Now that we have explored the concept of scopes, this leads naturally to lifetimes, as lifetimes can be defined by scopes. Remember that a borrow is not sole ownership. Because of this, there is a risk that we could reference a variable that's deleted. This can be demonstrated in the following classic demonstration of a lifetime:

fn main() {
    let one;
    {
        let two: i8 = 2;
        one = &two;
    } // -----------------------> two lifetime stops here
    println!("r: {}", one);
}

Running this code gives us the following error:

    one = &two;
     ^^^^ borrowed value does not live long enough
} // -----------------------> two lifetime stops here
- 'two' dropped here while still borrowed
println!("r: {}", one);
                  --- borrow later used here

What has happened here is that we state that there is a variable called one. We then define an inner-scope. Inside this scope, we define an integer two. We then assign one to be a reference of two. When we try and print one in the outer-scope, we can't, as the variable it is pointing to has been deleted. Therefore, we no longer get the issue that the variable is out of scope, it's that the lifetime of the value that the variable is pointing to is no longer available, as it's been deleted. The lifetime of two is shorter than the lifetime of one.

While it is great that this is flagged when compiling, Rust does not stop here. This concept also translates functions. Let's say that we build a function that references two integers, compares them, and returns the highest integer reference. The function is an isolated piece of code. In this function, we can denote the lifetimes of the two integers. This is done by using the ' prefix, which is a lifetime notation. The names of the notations can be anything you wish, but it's a general convention to use a, b, c, and so on. Let's look at an example:

fn get_highest<'a>(first_number: &'a i8, second_number: &'\
  a     i8) -> &'a i8 {
    if first_number > second_number {
        return first_number
    } else {
        return second_number
    }
}
fn main() {
    let one: i8 = 1;
    {
        let two: i8 = 2;
        let outcome: &i8 = get_highest(&one, &two);
        println!("{}", outcome);
    }
}

As we can see, the first_number and second_number variables have the same lifetime notation of a. This means that they have the same lifetimes. We also have to note that the get_highest function returns an i8 with a lifetime of a. As a result, both first_number and second_number variables can be returned, which means that we cannot use the outcome variable outside of the inner-scope. However, we know that our lifetimes between the variables one and two are different. If we want to utilize the outcome variable outside of the inner-scope, we must tell the function that there are two different lifetimes. We can see the definition and implementation here:

fn get_highest<'a, 'b>(first_number: &'a i8, second_ \
  number:   &'b i8) -> &'a i8 {
    if first_number > second_number {
        return first_number
    } else {
        return &0
    }
}
fn main() {
    let one: i8 = 1;
    let outcome: &i8;
    {
        let two: i8 = 2;
        outcome = get_highest(&one, &two);
    }
    println!("{}", outcome);
}

Again, the lifetime a is returned. Therefore, the parameter with the lifetime b can be defined in the inner-scope as we are not returning it in the function. Considering this, we can see that lifetimes are not exactly essential. We can write comprehensive programs without touching lifetimes. However, they are an extra tool. We don't have to let scopes completely constrain us with lifetimes.

We are now at the final stages of knowing enough Rust to be productive Rust developers. All we need to understand now is building structs and managing them with macros. Once this is done, we can move onto the next chapter of structuring Rust programs. In the next section, we will cover the building of structs.

 

Building structs instead of objects

In Python, we use a lot of objects. In fact, everything you work with in Python is an object. In Rust, the closest thing we can get to objects is structs. To demonstrate this, let's build an object in Python, and then replicate the behavior in Rust. For our example, we will build a basic stock object as seen in the following code:

class Stock:
    def __init__(self, name: str, open_price: float,\
      stop_loss: float = 0.0, take_profit: float = 0.0) \
        -> None:
        self.name: str = name
        self.open_price: float = open_price
        self.stop_loss: float = stop_loss
        self.take_profit: float = take_profit
        self.current_price: float = open_price
    def update_price(self, new_price: float) -> None:
        self.current_price = new_price

Here, we can see that we have two mandatory fields, which are the name and price of the stock. We can also have an optional stop loss and an optional take profit. This means that if the stock crosses one of these thresholds, it forces a sale, so we don't continue to lose more money or keep letting the stock rise to the point where it crashes. We then have a function that merely updates the current price of the stock. We could add extra logic here on the thresholds for it to return a bool for whether the stock should be sold or not if needed. For Rust, we define the fields with the following code:

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

Now we have our fields for the struct, we need to build the constructor. We can build functions that belong to our struct with an impl block. We build our constructor with the following code:

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

Here, we can see that we have defined some default values for some of the attributes. To build an instance, we use the following code:

let stock: Stock = Stock::new("MonolithAi", 95.0);

However, we have not exactly replicated our Python object. In the Python object __init__, there were some optional parameters. We can do this by adding the following functions to our impl block:

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

What we do here is take in our struct, mutate the field, and then return it. Building a new stock with a stop loss involves calling our constructor followed by the with_stop_loss function as seen here:

let stock_two: Stock = Stock::new("RIMES",\
    150.4).with_stop_loss(55.0);

With this, our RIMES stock will have an open price of 150.4, current price of 150.4, and a stop loss of 55.0. We can chain multiple functions as they return the stock struct. We can create a stock struct with a stop loss and a take profit with the following code:

let stock_three: Stock = Stock::new("BUMPER (former known \
  as ASF)", 120.0).with_take_profit(100.0).\
    with_stop_loss(50.0);

We can continue chaining with as many optional variables as we want. This also enables us to encapsulate the logic behind defining these attributes. Now that we have all our constructor needs sorted, we need to edit the update_price attribute. This can be done by implementing the following function in the impl block:

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

This can be implemented with the following code:

let mut stock: Stock = Stock::new("MonolithAi", 95.0);
stock.update_price(128.4);
println!("here is the stock: {}", stock.current_price);

It has to be noted that the stock needs to be mutable. The preceding code gives us the following printout:

here is the stock: 128.4

There is only one concept left to explore for structs and this is traits. As we have stated before, traits are like Python mixins. However, traits can also act as a data type because we know that a struct that has the trait has those functions housed in the trait. To demonstrate this, we can create a CanTransfer trait in the following code:

trait CanTransfer {
    fn transfer_stock(&self) -> ();
    
    fn print(&self) -> () {
        println!("a transfer is happening");
    }
}

If we implement the trait for a struct, the instance of the struct can utilize the print function. However, the transfer_stock function doesn't have a body. This means that we must define our own function if it has the same return value. We can implement the trait for our struct using the following code:

impl CanTransfer for Stock {
    fn transfer_stock(&self) -> () {
        println!("the stock {} is being transferred for \
          £{}", self.name, self.current_price);
    }
}

We can now use our trait with the following code:

let stock: Stock = Stock::new("MonolithAi", 95.0);
stock.print();
stock.transfer_stock();

This gives us the following output:

a transfer is happening
the stock MonolithAi is being transferred for £95

We can make our own function that will print and transfer the stock. It will accept all structs that implement our CanTransfer trait and we can use all the trait's functions in it, as seen here:

fn process_transfer(stock: impl CanTransfer) -> () {
    stock.print();
    stock.transfer_stock();
}

We can see that traits are a powerful alternative to object inheritance; they reduce the amount of repeated code for structs that fit in the same group. There is no limit to the number of traits that a struct can implement. This enables us to plug traits in and out, adding a lot of flexibility to our structs when maintaining code.

Traits are not the only way by which we can manage how structs interact with the rest of the program; we can achieve metaprogramming with macros, which we will explore in the next section.

 

Metaprogramming with macros instead of decorators

Metaprogramming can generally be described as a way in which the program can manipulate itself based on certain instructions. Considering the strong typing Rust has, one of the simplest ways that we can metaprogram is by using generics. A classic example of demonstrating generics is through coordinates:

struct Coordinate <T> {
        x: T,
        y: T
    }
fn main() {
    let one = Coordinate{x: 50, y: 50};
    let two = Coordinate{x: 500, y: 500};
    let three = Coordinate{x: 5.6, y: 5.6};
}

What is happening here is that the compiler is looking through all the uses of our struct throughout the whole program. It then creates structs that have those types. Generics are a good way of saving time and getting the compiler to write repetitive code. While this is the simplest form of metaprogramming, another form of metaprogramming in Rust is macros.

You may have noticed throughout the chapter that some of the functions that we use, such as the println! function, have an ! at the end. This is because it is not technically a function, it is a macro. The ! denotes that the macro is being called. Defining our own macros is a blend of defining our own function and using lifetime notation within a match statement within the function. To demonstrate this, we can define our own macro that capitalizes the first character in a string passed through it with the following code:

macro_rules! capitalize {
        ($a: expr) => {
            let mut v: Vec<char> = $a.chars().collect();
            v[0] = v[0].to_uppercase().nth(0).unwrap();
            $a = v.into_iter().collect();
        }
    }
fn main() {
    let mut x = String::from("test");
    capitalize!(x);
    println!("{}", x);
}

Instead of using the fn term that is used for defining functions, we define our macro using macro_rules!. We then say that the $a is the expression passed into the macro. We then get the expression, convert it into a vector of chars, uppercase the first character, and then convert it back to a string. It must be noted that the macro that we defined does not return anything, and we do not assign any variable when calling our macro in the main function. However, when we print the x variable at the end of the main function, it is capitalized. Therefore, we can deduce that our macro is altering the state of the variable.

However, we must remember that macros are a last resort. Our example shows that our macro alters the state even though it is not directly demonstrated in the main function. As the complexity of the program grows, we could end up with a lot of brittle, highly coupled processes that we are not aware of. If we change one thing, it could break five other things. For capitalizing the first letter, it is better to just build a function that does this and returns a string value.

Macros do not just stop at what we have covered, they also have the same effect as our decorators in Python. To demonstrate this, let's look at our coordinate again. We can generate our coordinate and then pass it through a function so it can be moved. We then try to print the coordinate outside of the function with the following code:

struct Coordinate {
    x: i8,
    y: i8
}
fn print(point: Coordinate) {
    println!("{} {}", point.x, point.y);
}
fn main() {
    let test = Coordinate{x: 1, y:2};
    print(test);
    println!("{}", test.x)
}

It will be expected that Rust will refuse to compile the code because the coordinate has been moved into the scope of the print function that we created and therefore we cannot use it in the final println!. We could borrow the coordinate and pass that through to the function. However, there is another way we can do this. Remember that integers passed through functions without any trouble because they had a Copy trait. Now, we could try and code a Copy trait ourselves, but this would be convoluted and would require advanced knowledge. Luckily for us, we can implement the Copy and Clone traits by utilizing a derive macro with the following code:

#[derive(Clone, Copy)]
struct Coordinate {
        x: i8,
        y: i8
    }

With this, our code works as we copy the coordinate when passing it through the function. Macros can be utilized by many packages and frameworks, from JavaScript Object Notation (JSON) serialization to entire web frameworks. In fact, here is the classic example of running a basic server in the Rocket framework:

#![feature(proc_macro_hygiene, decl_macro)]
#[macro_use] extern crate rocket;
#[get("/hello/<name>/<age>")]
fn hello(name: String, age: u8) -> String {
    format!("Hello, {} year old named {}!", age, name)
}
fn main() {
    rocket::ignite().mount("/", routes![hello]).launch();
}

This is a striking resemblance to the Python Flask application example at the beginning of the chapter. These macros are acting exactly like our decorators in Python, which is not surprising as a decorator in Python is a form of metaprogramming that wraps a function.

This wraps up our brief introduction to the Rust language for Python developers. We are now able to move on to other concepts, such as structuring our code and building fully fledged programs coded in Rust.

 

Summary

In this chapter, we explored the role of Rust in today's landscape, showing that Rust's paradigm-changing position is a result of being memory-safe, while not having any garbage collection. With this, we understood why it beats most languages (including Golang) when it comes to speed. We then went over the quirks that Rust has when it comes to strings, lifetimes, memory management, and typing, so we can write safe and efficient Rust code as Python developers. We then covered structs and traits to the point where we could mimic the basic functionality of a Python object with mixins, utilizing their traits as types for the Rust struct while we were at it.

We covered the basic concepts of lifetimes and borrowing. This enables us to have more control over how we implement our structs and functions within our program, giving us multiple avenues to turn to when solving a problem. With all this, we can safely code single-page applications with confidence over concepts that would stump someone who has never coded in Rust. However, we know, as experienced Python developers, that any serious program worth coding spans multiple pages. Considering this, we can use what we have learned here to move on to the next chapter, where we set up a Rust environment on our own computers and learn how to structure Rust code over multiple files, enabling us to get one step closer to building packages in Rust and installing them with pip.

 

Questions

  1. Why can we not simply copy a String?
  2. Rust has strong typing. In which two ways can we enable a container such as a vector or hashmap to contain multiple different types?
  3. How are Python decorators and Rust macros the same?
  4. What is the Python equivalent to a main function in Rust?
  5. Why can we get a higher integer value with the same number of bytes with an unsigned integer than a signed integer?
  6. Why do we have to be strict with lifetimes and scopes when coding in Rust?
  7. Can we reference a variable when it has been moved?
  8. What can you do to an original variable if it is currently being borrowed in an immutable state?
  9. What can you do to an original variable if it is currently being borrowed in a mutable state?
 

Answers

  1. This is because a String is essentially a pointer to Vec<u8> with some metadata. If we copy this, then we will have multiple unconstrained pointers to the same string literal, which will introduce errors with concurrency, mutability, and lifetimes.
  2. We can use an Enum, which means that the type being accepted into the container can be one of those types housed in the Enum. When reading the data, we can then use a match statement to manage all possible data types that could be read from the container. The second way is to create a trait that multiple different structs implement. However, the only interaction that we can have from the container read when this is the case is the functions that the trait implements.
  3. They both wrap around the code and alter the implementation or attributes of the code that they are wrapping without directly returning anything.
  4. The Python equivalent is if __name__ == "__main__":.
  5. A signed integer must accommodate positive and negative values, whereas an unsigned integer only accommodates positive values.
  6. This is because there is no garbage collection; as a result, variables get deleted when they shift out of the scope of where they were created. If we do not consider lifetimes, we could reference a variable that has been deleted.
  7. No, the ownership of the variable has essentially been moved and there are no references to the original variable anymore.
  8. We can still copy and borrow the original variable; however, we cannot perform a mutable borrow.
  9. We cannot use the original variable at all as the state of the variable might be altered.
 

Further reading

  • Hands-On Functional Programming in Rust (2018) by Andrew Johnson, Packt Publishing
  • Mastering Rust (2019) by Rahul Sharma and Vesa Kaihlavirta, Packt Publishing
  • The Rust Programming Language (2018): https://doc.rust-lang.org/stable/book/
About the Author
  • Maxwell Flitton

    Maxwell Flitton is a software engineer who works for the open source financial loss modeling foundation OasisLMF. In 2011, Maxwell achieved his Bachelor of Science degree in nursing from the University of Lincoln, UK. While working 12-hour shifts in the A&E departments of hospitals, Maxwell obtained another degree in physics from the Open University in the UK and then moved on to another milestone, with a postgraduate diploma in physics and engineering in medicine from UCL in London. He's worked on numerous projects such as medical simulation soft ware for the German government and supervising computational medicine students at Imperial College London. He also has experience in financial tech and Monolith AI.

    Browse publications by this author
Latest Reviews (4 reviews total)
Szeretném a Python környezetben nagy adatállományok feldolgozását gyorsítani, minden olyan eszköz lehetőség ami ebben lehetőséget rejt az érdekel és ki szeretném próbálni.
The book has been really good for picking up Rust from a Pythonista perspective. It does a comprehensive job at walking through setups for those that might not have done Python package management before, but it may be good still to come with some experience to pick things up quicker. Overall, would recommend!
Same as the previous book
Speed Up Your Python with Rust
Unlock this book and the full library FREE for 7 days
Start now