07 Rustlings Move Semantics Part 2 Solution

From the Rustlings ReadMe: These exercises are adapted from pnkfelix's Rust Tutorial -- Thank you Felix!!!

Further information

For this section, the book links are especially important.

Move Semantics Part 2

We've already covered the first three exercises of Move Semantics in part one, in this episode we tackle the next three. Let's get started!

move_semantics4.rs

// move_semantics4.rs
// Refactor this code so that instead of passing `vec0` into the `fill_vec` function,
// the Vector gets created in the function itself and passed back to the main
// function.
// Execute `rustlings hint move_semantics4` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

fn main() {
    let vec0 = Vec::new();

    let mut vec1 = fill_vec(vec0);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

// `fill_vec()` no longer takes `vec: Vec<i32>` as argument
fn fill_vec() -> Vec<i32> {
    let mut vec = vec;

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

Our instructions here are to refactor our code so that instead of passing vec0 into the fill_vec function, the Vector gets created in the function itself and passed back to the main function.

move_semantics4.rs errors

⚠️  Compiling of exercises/move_semantics/move_semantics4.rs failed! Please try again. Here is the output:
error[E0423]: expected value, found macro `vec`
  --> exercises/move_semantics/move_semantics4.rs:23:19
   |
23 |     let mut vec = vec;
   |                   ^^^ not a value

error[E0061]: this function takes 0 arguments but 1 argument was supplied
  --> exercises/move_semantics/move_semantics4.rs:12:20
   |
12 |     let mut vec1 = fill_vec(vec0);
   |                    ^^^^^^^^ ---- argument of type `Vec<_>` unexpected
   |
note: function defined here
  --> exercises/move_semantics/move_semantics4.rs:22:4
   |
22 | fn fill_vec() -> Vec<i32> {
   |    ^^^^^^^^
help: remove the extra argument
   |
12 |     let mut vec1 = fill_vec();
   |                            ~~

error: aborting due to 2 previous errors

Error's are telling us that on line 23, vec is not a value in addition it tells us that the function fill_vec() takes 0 arguments but one is being supplied. So let's remove the argument on line 12 from the fill_vec(vec0).

After we do that we still get some errors, but they're different now.

⚠️  Compiling of exercises/move_semantics/move_semantics4.rs failed! Please try again. Here is the output:
error[E0423]: expected value, found macro `vec`
  --> exercises/move_semantics/move_semantics4.rs:23:19
   |
23 |     let mut vec = vec;
   |                   ^^^ not a value

error[E0282]: type annotations needed for `Vec<T>`
  --> exercises/move_semantics/move_semantics4.rs:10:9
   |
10 |     let vec0 = Vec::new();
   |         ^^^^
   |
help: consider giving `vec0` an explicit type, where the type for type parameter `T` is specified
   |
10 |     let vec0: Vec<T> = Vec::new();
   |             ++++++++

error: aborting due to 2 previous errors

The error's start point us to using generics with Vec<T> but our solution should be must simpler than that, since our instructions are to refactor our code to not create vec0 but to create a new vector in our fill_vec() function.

Let's see what happens if we simple remove the let vec0 = Vec::new(); line from our code but and add the let mut vec = Vec::new(); to our fill_vec() function. Hey guess what? It works. Below is the update code solution.

move_semantics4 solution

fn main() {
	// removed previous vector creation here
    let mut vec1 = fill_vec();

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);

    vec1.push(88);

    println!("{} has length {} content `{:?}`", "vec1", vec1.len(), vec1);
}

fn fill_vec() -> Vec<i32> {
    let mut vec = Vec::new(); // added vector creation within the function

    vec.push(22);
    vec.push(44);
    vec.push(66);

    vec
}

This is our output:

Output:
====================
vec1 has length 3 content `[22, 44, 66]`
vec1 has length 4 content `[22, 44, 66, 88]`

====================

move_semantics5.rs

// move_semantics5.rs
// Make me compile only by reordering the lines in `main()`, but without
// adding, changing or removing any of them.
// Execute `rustlings hint move_semantics5` or use the `hint` watch subcommand for a hint.

// I AM NOT DONE

fn main() {
    let mut x = 100;
    let y = &mut x;
    let z = &mut x;
    *y += 100;
    *z += 1000;
    assert_eq!(x, 1200);
}

This looks like an easy one, no need to write anything we just need to re-order the sequence of the lines. Let's look at the errors and see if we can get any hints on what we need to do.

move_semantics5.rs errors

⚠️  Compiling of exercises/move_semantics/move_semantics5.rs failed! Please try again. Here is the output:
error[E0499]: cannot borrow `x` as mutable more than once at a time
  --> exercises/move_semantics/move_semantics5.rs:11:13
   |
10 |     let y = &mut x;
   |             ------ first mutable borrow occurs here
11 |     let z = &mut x;
   |             ^^^^^^ second mutable borrow occurs here
12 |     *y += 100;
   |     --------- first borrow later used here

The Rust compiler tells us that we are borrowing x mutably too many times, so let's go step by step and see what is happening on each line.

  1. We declare a mutable variable x and assign it the value 100.
  2. We create a mutable reference y that borrows x using &mut x.
  3. Then, we create another mutable reference z that also borrows x using &mut x. This is where the problem arises.
    • The rules of borrowing state that you can have either one mutable reference or any number of immutable references to a value at a given time.
    • In this case, we already have y as a mutable reference to x, so we can't create another mutable reference z.
  4. The code tries to dereference y using *y and add 100 to the value of x. This is invalid because y is still in scope and holds a mutable reference to x, and at this point, z also exists.
  5. Similarly, when the code tries to dereference z using *z and add 1000 to the value of x, it violates the borrowing rules.

So how do we solve this? Let's try by dereferencing y before we try to borrow it again with z by moving the line *y += 100; above the 2nd attempt mutable borrow which is let z = &mut x;. Doing so should allow us to compile.

move_semantics5.rs solution

fn main() {
    let mut x = 100;
    let y = &mut x;
    *y += 100;
    let z = &mut x;
    *z += 1000;
    assert_eq!(x, 1200);
}

It compiles! We don't get an output because there is not println! statement instead we have an assert_eq!.

The code compiles successfully because it follows the borrowing rules in Rust. Here's a step-by-step explanation:

  1. We start by declaring a mutable variable x and assigning it the value 100.
  2. We then create a mutable reference y that borrows x using &mut x. This allows us to modify x through y.
  3. We dereference y using *y and add 100 to the value of x. This modifies x to 200.
  4. Next, we create another mutable reference z that also borrows x using &mut x. This is allowed because there are no other references to x at this point.
  5. We dereference z using *z and add 1000 to the value of x. This modifies x to 1200.
  6. Finally, we use assert_eq! to check if x is equal to 1200. Since the value of x is indeed 1200, the assertion passes.

Let's move on to our final move_semantics exercise.

move_semantics6.rs

// move_semantics6.rs
// Execute `rustlings hint move_semantics6` or use the `hint` watch subcommand for a hint.
// You can't change anything except adding or removing references.

// I AM NOT DONE

fn main() {
    let data = "Rust is great!".to_string();

    get_char(data);

    string_uppercase(&data);
}

// Should not take ownership
fn get_char(data: String) -> char {
    data.chars().last().unwrap()
}

// Should take ownership
fn string_uppercase(mut data: &String) {
    data = &data.to_uppercase();

    println!("{}", data);
}

Our instructions are to not change anything but the references, so we'll look at the errors to get a better understanding as to where we are having issues.

move_semantics6.rs errors

⚠️  Compiling of exercises/move_semantics/move_semantics6.rs failed! Please try again. Here is the output:
error[E0382]: borrow of moved value: `data`
  --> exercises/move_semantics/move_semantics6.rs:12:22
   |
8  |     let data = "Rust is great!".to_string();
   |         ---- move occurs because `data` has type `String`, which does not implement the `Copy` trait
9  |
10 |     get_char(data);
   |              ---- value moved here
11 |
12 |     string_uppercase(&data);
   |                      ^^^^^ value borrowed here after move
   |
note: consider changing this parameter type in function `get_char` to borrow instead if owning the value is not necessary
  --> exercises/move_semantics/move_semantics6.rs:16:19
   |
16 | fn get_char(data: String) -> char {
   |    --------       ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
help: consider cloning the value if the performance cost is acceptable
   |
10 |     get_char(data.clone());
   |                  ++++++++

error[E0716]: temporary value dropped while borrowed
  --> exercises/move_semantics/move_semantics6.rs:22:13
   |
21 | fn string_uppercase(mut data: &String) {
   |                               - let us call the lifetime of this reference `'1`
22 |     data = &data.to_uppercase();
   |     --------^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
   |     |       |
   |     |       creates a temporary value which is freed while still in use
   |     assignment requires that borrow lasts for `'1`

error: aborting due to 2 previous errors

Alright let's go down the list understanding what there errors are telling us and see how we can fix them.

8  |     let data = "Rust is great!".to_string();
   |         ---- move occurs because `data` has type `String`, which does not implement the `Copy` trait
9  |
10 |     get_char(data);
   |              ---- value moved here
11 |
12 |     string_uppercase(&data);
   |                      ^^^^^ value borrowed here after move
   note: consider changing this parameter type in function `get_char` to borrow instead if owning the value is not necessary
  --> exercises/move_semantics/move_semantics6.rs:16:19

here the compiler is telling us clearly where to look data does not implement the copy trait so we when we pass it through as a parameter in get_char(data), it becomes owned by get_char()

In the next batch of errors we get a suggestion about cloning, but we know that we can't change any of the code other than changing the references, so this is not the path we want to take.

16 | fn get_char(data: String) -> char {
   |    --------       ^^^^^^ this parameter takes ownership of the value
   |    |
   |    in this function
help: consider cloning the value if the performance cost is acceptable
   |
10 |     get_char(data.clone());
   |                  ++++++++

error[E0716]: temporary value dropped while borrowed
 |
21 | fn string_uppercase(mut data: &String) {
   |                               - let us call the lifetime of this reference `'1'`
22 |     data = &data.to_uppercase();
   |     --------^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
   |     |       |
   |     |       creates a temporary value which is freed while still in use
   |     assignment requires that borrow lasts for `'1`

We now get a message about the lifetime of a reference which we haven't covered yet so let's just keep this in mind for now, but again our task is to essentially just change how the functions handle ownership. So let's go back and look at the functions in the code:

// Should not take ownership
fn get_char(data: String) -> char {
    data.chars().last().unwrap()
}

// Should take ownership
fn string_uppercase(mut data: &String) {
    data = &data.to_uppercase();

    println!("{}", data);
}

Looking at these two code blocks it looks straightforward, it's clear that we must change where the & symbol is being used and essentially swap positions in each function to this:

// Should not take ownership
fn get_char(data: &String) -> char {
    data.chars().last().unwrap()
}

// Should take ownership
fn string_uppercase(mut data: String) {
    data = data.to_uppercase();

    println!("{}", data);
}

Once we've done this we get new error's but it should be pretty clear what we need to do in the fn main()

⚠️  Compiling of exercises/move_semantics/move_semantics6.rs failed! Please try again. Here is the output:
error[E0308]: mismatched types
  --> exercises/move_semantics/move_semantics6.rs:10:14
   |
10 |     get_char(data);
   |     -------- ^^^^
   |     |        |
   |     |        expected `&String`, found struct `String`
   |     |        help: consider borrowing here: `&data`
   |     arguments to this function are incorrect
   |
note: function defined here
  --> exercises/move_semantics/move_semantics6.rs:16:4
   |
16 | fn get_char(data: &String) -> char {
   |    ^^^^^^^^ -------------

error[E0308]: mismatched types
  --> exercises/move_semantics/move_semantics6.rs:12:22
   |
12 |     string_uppercase(&data);
   |     ---------------- ^^^^^ expected struct `String`, found `&String`
   |     |
   |     arguments to this function are incorrect
   |
note: function defined here
  --> exercises/move_semantics/move_semantics6.rs:21:4
   |
21 | fn string_uppercase(mut data: String) {
   |    ^^^^^^^^^^^^^^^^ ----------------
help: consider removing the borrow
   |
12 -     string_uppercase(&data);
12 +     string_uppercase(data);
   |

error: aborting due to 2 previous errors

The compiler gives us great information on what we should do literally showing us what we can do to make the code compile. So let's try it.

move_semantics6.rs solution

fn main() {
    let data = "Rust is great!".to_string();

    get_char(&data);

    string_uppercase(data);
}

// Should not take ownership
fn get_char(data: &String) -> char {
    data.chars().last().unwrap()
}

// Should take ownership
fn string_uppercase(mut data: String) {
    data = data.to_uppercase();

    println!("{}", data);
}

There we have it our solution is to again swap the & symbol's position to match that of the function's signature to make sure that we are borrowing and taking ownership as the function expects. With that we get our print out:

Output:
====================
RUST IS GREAT!

====================

Conclusion

Rust's move semantics are important for understanding memory management and ownership in the language. By leveraging references, borrowing, and ownership, Rust ensures memory safety and eliminates many common programming errors like null pointer dereferences and dangling references.

In this blog post, we explored three exercises related to move semantics. We refactored code, re-ordered lines, and adjusted ownership to solve the problems. Through these exercises, we gained a better understanding of how move semantics work in Rust and how to manipulate ownership and references effectively.

Move semantics play a crucial role in Rust's design philosophy, enabling high-performance and safe code without sacrificing expressiveness. By embracing move semantics and mastering the intricacies of ownership, borrowing, and references, Rust developers can write robust and efficient code.

Remember, practice is key to mastering move semantics and other advanced features of Rust. Keep exploring, experimenting, and building projects to deepen your understanding and become a proficient Rust programmer. Happy coding!