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;