Cookie Consent by Privacy Policies Generator Vegapit
Mutability in Rust for functions and closures
Last updated: 2020-01-07T20:33:55

The aim of the exercise is simple. Start with an empty vector of unsigned integers - as it does not implement the Copy Trait - and create different types of functions and closures to push a new number to the original vector. Now that the goal is set, let’s create and instantiate an empty vector:

let mut v : Vec<u8> = Vec::new();

println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: []

We will keep track of the address of the v variable to ensure the same instance is used throughout. Let’s introduce the first function that serves the purpose.

Function on a mutable variable

fn push1(mut x : Vec<u8>) -> Vec<u8> {
	x.push(1);
    x
}

v = push1(v);

println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1]

How does push1 work exactly? Well, we can say based on its signature that:

  • It takes ownership of the passed variable x (by definition)
  • It is allowed to modify the passed variable x (because of the mut keyword)

Calling push1(x) means that x moves to the function’s scope, and gets destroyed right after the function has finished executing. So we need to return the modified variable and reassign it if we want to be able to use it in the rest of the program. As the printed output shows, the address has been preserved proving that we worked on the same vector instance.

Function on a mutable reference

fn push2(x : &mut Vec<u8>) {
	x.push(2);
}

push2(&mut v);

println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1,2]

The function push2 takes a mutable reference as argument. Its signature tells us that:

  • It borrows the passed reference x (by defintion)
  • It is allowed to modify the passed reference x (because of the mut keyword)

Since the variable v is of type Vec<u8>, we need to cast it into a mutable reference during the function call by passing &mut v as argument. The vector address remains unchanged based on the very definition of variable referencing. Let’s now take a look at closures.

Closure using a mutable variable

let mut push3 = || v.push(3);

push3();

println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1,2,3]

As opposed to functions, closures have access to variables defined outside their scope. The closure push3 is defined as mutable which means that:

  • It has direct access to the original variable v (same as borrowing in effect)
  • It is allowed to modify the original variable v (thanks to the mut keyword)

Since the closure works directly on the instance of variable v, the address is indeed the same post execution.

Closure with “move” keyword

let push4 = move || {
    v.push(4);
    v
};

v = push4();

println!("Address: {:p}, Content: {:?}", &v, &v);
// Address: 0xb62dfffb10, Content: [1,2,3,4]

The move keyword creates a closure that takes ownership of the variables defined outside its scope, if they are being used within the closure. We can then say about the closure that:

  • It takes ownership of the variable v (because of the move keyword)
  • It is allowed to modify the variable v (because v is mutable)

Similarly to the push1 function, because it took ownership of the variable v, the closure needs to return the modified vector so it can be reassigned and used in the rest of the program.

If you like this post, follow me on Twitter and get notified on the next posts.