N00b understanding scopes

Hi!

While reading the section What is Ownership? on Chapter 4 from the book, I got to the final example:

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String
    (s, length)
}

Which works as expected. I then tried to be smarter than that, and "simplified" the calculate_length function like this:

fn calculate_length(s: String) -> (String, usize) {
    (s, s.len())
}

Which gives me a compiler error, the gist of it being the use of a moved value:

8 |     (s, s.len())
  |      -  ^ value used here after move
  |      |
  |      value moved here

And that's what I don't understand. Isn't the function calculate_length a whole scope where s is valid? Why did s moved when constructing the final tuple as expression?

Thanks!

1 Like

In rust when you pass a variable (and not a reference of a variable) that variable is always moved. In your case it has been moved into the first element of the tuple. The next statement would be to fill in the second part of the tuple and the compiler complains that s has been moved away already. Rust enforces those rules to guarantee that there is only one owner. The original owner was outside of the function. After that the owner became calculate_length and last the owner is the tuples first element.

If you know C++ you can think of String only having a move constructor and no copy constructor.
The u32 also has the copy trait (analog to a C++ copy constructor). Therefore the length can be copied of first but the String has already been moved away.

2 Likes

Rust is just super pedantic about order of evaluation.

When it builds the first argument of the tuple it moves s from your scope into the tuple, so when it comes to evaluate the second argument it sees that s is already "gone" and belongs to the tuple, not the variable.

4 Likes

Right, and this compiles:

fn calculate_length(s: String) -> (usize, String) {
    (s.len(), s)
}
3 Likes

Please don't depend on the order of evaluation.

2 Likes

Yes. Reading this whole section of the book made that clear for me (including running the examples and my own variations). I came to understand this ownership deal with regards to scopes, which I thought I could easily identify, as they're variable visibility perimeters. That's why I thought

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String
    (s, length)
}

was just one scope.

And that's what makes my understanding wrong: it seems as if the tuple... is a new scope? I'm trying to wrap my head around this, despite your comment (and @kornel's) I'm not finding this terribly intuitive. Still, thanks!

I reduced my test case to

fn main() {
    let message = String::from("Hello, world!");
    let population = 7.442;

    let tuple = (message, population);

    println!("message from tuple: '{}'", tuple.0);
    println!("  original message: '{}'", message);
}

Which won't compile, making it obvious that the tuple takes ownership of the variable, just like @Arne wrote. By defining the tuple, I'm invalidating the original variable. What's happening here could be equivalent (-ish) to

fn main() {
    let message = String::from("Hello, world!");

    let text = message;

    println!("   text: '{}'", text);
    println!("message: '{}'", message);
}

This won't compile as message was moved to text, which is literally explained earlier in the book. I think I got it.

It's the order of the operations that matters here. Don't think of the tuple as a new scope; rather view your code as a sequence of instructions to the compiler:

  1. Move the function argument s into the first element of the tuple, making the argument inaccessible via future references to the name s.
  2. Take the length of the [no longer accessible] function argument s and move it into the second element of the tuple.
5 Likes

Yes! Thank you!

Existence of variables is strictly limited to their scope, but objects owned by these variables can change owners (and the new owner can be a new variable in a new scope, but also a temporary object or something else without an obvious scope).

Ownership has to be tracked more precisely than scope of variables, e.g. loops need to track ownership for each iteration of the loop, even though there's only one scope.

1 Like

How would you explain example by leonardo (with different order in tuple) ? Which works fine.

Expression evaluation order is "in order of appearance". In other words Left to Right and Top to Bottom. And each element of a tuple is a different expression which are separated left to right by the comma.

Thx, I must read about borrowing some more.