Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Chapter 2: Advanced Rust Concepts

Ownership and Borrowing

Next, we’d want to call the function that handles user input in accordance with the previously initialized variables.

We’ll momentarily leave the main function and head to the handle_input function:

Before we do that, let’s take a look at the concept of Ownership and Borrowing in Rust

Ownership

Take a look at this short snippet:

fn main() { //for brevity sake, we won’t specify this main function in subsequent snippets
	let hello = “hello”;
} // at this point, hello no longer exists.

By default, variables are block-scoped. Hello is an example of what we call a string literal: hardcoded string values.

let hello = String::from(“hello”);
let bye = hello;
println!(“{}”, hello); //the remaining arguments to the println macro are variables that would replace ‘{}’ in the string.

Running this code would throw an error(called panicking in Rust):

error[E0382]: borrow of moved value: `hello`
 --> main.rs:4:20
  |
2 |     let hello = String::from("hello");
  |         ----- move occurs because `hello` has type `std::string::String`, which does not implement the `Copy` trait
3 |     let bye = hello;
  |               ----- value moved here
4 |     println!("{}", hello);
  |                    ^^^^^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try rustc --explain E0382.

This signifies a concept called Moving.

This means in Rust, an attempt to free unused memory, since both hello and bye are pointing to the same memory address, will invalidate the variable hello.

Any attempt to call the variable would result in an error. As a systems programming language with a focus on memory safety, this is a useful feature that prevents access to a memory address after it is no longer needed.

Rust performs a form of copying, known as shallow copying. This way, both variables reference the same thing.

In another example shown below:

let hello = String::from(“hello”);
let bye = “ and bye”;
let greeting = hello + bye;
println!(“{}. Again, {}? {}”, greeting, hello, bye);

Again, as expected, this code won’t compile. bye ceases to exist the moment it is used somewhere else.

The new variable takes ownership of bye and bye ceases to exist. hello on the other hand, has no problems with this. This is because, due to the use of String::new(), the size of hello is known at compile-time, therefore, there’s no deep or shallow copying here.

Using the code above, we can solve this problem by performing explicit deep copying like so:

let hello = String::from(“hello”);
let bye = hello.copy();
println!(“{}”, hello);

This time we’d have no errors since the underlying data is copied to bye and ownership of bye isn’t performed.

Borrowing

How about functions?

How do we pass arguments into functions and still reuse the variables? Rust has some sort of pointer-like magic called Borrowing.

With it, we can pass to functions, references to variables and access underlying data, and even mutate it (as we’ll see in the handle_input function).

Let’s go on to create the handle_input function and learn Borrowing from it:

fn handle_input(var: &mut String) → i32 {
	io::stdin()
        .read_line(var)
        .expect("Failed to read line");

	var.chars()
	    .count()
	    .try_into()
	    .expect("Length of name is too large!")
}

The function accepts a funny argument type, &mut String. You’ll probably recognize it as expecting a mutable String as an argument, but it goes farther than that:

The & sign indicates that the function is expecting a reference to that data type since we’d like to mutate its value rather than take ownership of it as Rust would normally expect.

This concept is called Borrowing.

At the end of the function’s execution, control of the memory address is returned back to the initial variable.

So, we call the stdin method of the io module for input-related functionalities, read_line does the magic of passing the user input into the variable specified.

The expect method is used for error propagation:

There are cases where we’d want our function to return an error type to the calling code to handle if an error occurs while performing an operation.

The expect function (similar to unwrap ) allows us to specify a custom message for the panic (as exceptions are called in Rust).

While unwrap would instead return the error to the calling code without a custom message.

Return value (data types)

The handle_input function has a return type of i32, which is a signed 32-bit integer.

Other integer data types include i8, i16, i64, i128 for signed integers of 8, 16, 64 and 128 respectively.

Unsigned integer types are of the u prefix with variants such as u8, u16, u32, u64, and u128 with their obvious precision levels.

For floating-point values, only two types are available: f32 and f64 for 32 and 64-bit precision respectively.

Taking a look at the return value snippet:

var.chars()
	    .count()
	    .try_into()
	    .expect("Length of name is too large!")

The chars method first breaks the text given by the user into an array of individual characters, count gives its count with a type of usize, an integer type for the size of data in Rust.

The try_into function on the other hand attempts to convert usize type into i32.

You would notice that try_into automatically knows the type we need due to the function’s return type – another Rust magic.

Finally, we wrap it all with an expect for possible errors that might occur.

Back to the main function

At this point, we can then attempt to call the function and initialized our variables like so:

println!(“Enter first person name: “);
let name1_count = get_input(&mut name1);

println!(“Enter second person name: “);
let name2_count = get_input(&mut name2);

Matches

Next, we’ll need a function to compute the compatibility:

fn calculate_compatibility(name1_count: i32, name2_count: i32) → f32 {
	match name1_count.cmp(&name2_count) {
		Ordering::Less => {
			((name1_count as f32 / name2_count as f32) as f32 * 100.0) as f32
		},
		Ordering::Greater => {
			((name2_count as f32 / name1_count as f32) as f32 * 100.0) as f32
		},
		Ordering::Equal => {
			50.0
		}
	}
}

Since we’ll be doing a bit of arithmetic, the function should be returning a float type.

Next, we compare both values to see which is higher. To do that, instead of the if statement with a syntax of:

if <condition> {
	block of code
} else if <condition> {
	block of code
} else {
	block of code
}

We’ll be making use of match, since the comparison of two number in Rust yields a data type rather than a boolean:

match name1_count.cmp(&name2_count) {
	Ordering::Less => {
	},
	Ordering::Greater => {
	},
	Ordering::Equal => {
	}
}

A match statement is made up of match followed by a type, or an operation that returns a type. In the block of code following, it is arms with pathways of code to run depending on what type is expected.

With this functionality of types returned for comparison, we have to bring into scope, the types expected to be returned with the statement:

use std::cmp::Ordering;

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board