Sunday, January 05, 2020

Learning Rust - Day 2 - Getting Acquainted with Ownership.

So, I just finished day 2 of my learning Rust journey. You can read other posts in this series by following the label learning rust.

Note: this is the journaling of my experience learning Rust, where I pen down some of the insights I am picking up, things learnt, and things I can't yet wrap my head around in the language; not leaving out things I considered queer or inconvenient. As I said in the beginning post: The plan is that, after I become proficient in Rust, I can return to these series of posts and cringe at my ignorance!

This session saw me going through the 3rd and the 4th chapter of The book. The 3rd chapter, which is titled Common programming concepts was a breeze. The 4th chapter, which is about Understanding Ownership did not go that fast, as I encountered new ideas I had to let sink in. Below are some of the things that stood out for me at the end of the day:

You cannot declare and assign same value to multiple variables at a go in Rust. This wont work:

fn main() {
    let a = b = 10; // this wont work
}

Or any other variant of the above.

This has to do with the fact that assigning a value to a variable is a statement, and does not yield any value. I also got to confirm what I mentioned in Learning Rust - Day 1, about the fact that appending a line with ; signifies a statement in Rust, and absence of ; signifies an expression.

I also realised that the loop keyword is also an expression, and it is possible to return a value upon breaking out of the loop. The loop below would return 10 and that would be printed:

fn main() {
    let mut a = 0;
    let r = loop {
        a = a + 1;
        if a == 10 {
            break a
        }
    };

    println!("{}", r)
}


I also had to refresh my understanding of basic concepts around pointers, reference, stack and heaps. I found The 5 minutes guide to C pointers post particularly useful in motivating pointers and their syntax. This segued into Understanding ownership in Rust.

The key take away about ownership is the fact I now need to be mindful about assigning stuff to variables, and passing variables into a function, and what happens in terms of where things are in memory (stack or heap).

From the book:

All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead

This is the main point that distinguishes what goes on the stack and what goes on the heap. The next thing to know I guess is how to tell what are the data with fixed size and what are the ones whose size cannot be known at compile time.

Also previously I could get away with a mental model that sees variable assignment and passing arguments to a function as a by-value operation. i.e. values are copied and given to the new variable or function. When such operations happen, instead of thinking about values being passed, think about ownership being moved, where the "ownership content" can be thought of as the pointer to the memory location of the data, the current size of the data, and the reserved memory capacity in the heap.

With the above, then it becomes trivial to see why the following piece of code won't compile:

fn main() {
    let s = String::from("hello world");
    let ss = s; // ownership has moved from s to ss

    println!("{}", s); // s is now invalid since ownership moved
    println!("{}", ss);
}

compile error:

   Compiling chapter_3_4 v0.1.0 (/Users/chapter_3_4)
error[E0382]: borrow of moved value: `s`
  --> src/main.rs:11:20
   |
8  |     let s = String::from("hello world");
   |         - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
9  |     let ss = s; // ownership has moved from s to ss
   |              - value moved here
10 |
11 |     println!("{}", s); // s is now invalid since ownership moved
   |                    ^ value borrowed here after move

error: aborting due to previous error

For more information about this error, try `rustc --explain E0382`.
error: Could not compile `chapter_3_4`.

To learn more, run the command again with --verbose.

Process finished with exit code 101


Or why, even when applying borrowing via the use of &, the following won't compile:

fn main() {
    let mut s = String::from("hello world");
    let ss = &mut s;

    println!("{}, {}", ss, s);
}

nor this:

fn main() {
    let mut s = String::from("hello world");
    let ss = &mut s;

    println!("{}", s);
    println!("{}", ss);
}

But his will:

fn main() {
    let mut s = String::from("hello world");
    let ss = &mut s;

    println!("{}", ss);
    println!("{}", s);
}

and even the first example will compile with some slight modification (replacing let ss = &mut s with let ss = &s)

fn main() {
    let mut s = String::from("hello world");
    let ss = &s;

    println!("{}, {}", ss, s);
}

All these is due to the interplay between a mutable reference, and borrowing same reference down the line as immutable.

In general a high level summary of what I took away from the chapter about ownership is:

1. Understand scope, ownership and how ownership could be moved and lost due to assignment, passing into arguments and variable going out of scope

2. Understanding borrowing of ownership via the use of &, and the difference between mutable and immutable reference and the various rules that ensure these interplay safely.

The intuition I am getting is that in Rust, it is ensured that at any point in time, it is not possible for multiple variables to have ownership to same address on the heap memory, and in the case of reference, you have reference to ownership, and even though you can have multiple of these, the compiler ensures this is done in an orderly matter that does not lead to memory access bugs.

There were also a couple of tit bits I picked up:

Q. How do you silence the unused variable warning?
     A. By placing #![allow(unused_variables)] at the top of the function where the unused variable occurs, at the top of assignment of the unused variable, or by prepending the variable with an _

Q. Does Rust have something similar to typed holes like you have in Haskell or PureScript?
     A. Not quite. But there is a mechanism that comes close. Use _:(). For example: 
let _:() = String::from("some value"); The compiler error message of this will contain something like expected type `()` found type `std::string::String`.

That is it for now!

No comments: