loading...
Cover image for That's so Rusty: Mutables

That's so Rusty: Mutables

imaculate3 profile image Imaculate ・5 min read

Given its unique nature, a lot of people are curious to know if Rust is functional.

Search query

Is it? Let's find out.
For a programming language to be considered strictly functional it has to adhere to functional paradigm where functions are first class citizens and shared state, mutable data, and side-effects are not allowed. For that reason, a pure function will always return the same result for the same arguments. Rust supports Object Oriented Programming, which means it allows shared state. That disqualifies it from being a purely functional language. However, it borrows some functional concepts such as immutability.

All Rust variables are immutable ... by default.
There are a few ways to mutate them as shown below.

1. Mut keyword

fn main()
{
  let mut vec = vec![1, 5, 10, 2, 15];
  println!("The vector is: {:?}", vec);
  vec = vec![16, 2, 7, 5];
  println!("The vector is: {:?}", vec);
}

Above, the vector vec is declared mutable with the mut keyword, making it possible to change its contents.

2. Shadowing

fn main()
{  
  let s = String::from("Hello from");
  let s = String::from("A shadow");
}

Although it looks like s has been mutated, it hasn't. The let keyword declares another variable that happens to have the same name as existing one. This declaration overwrites the previous variable, effectively shadowing it.

So, you can still mutate things but it takes some effort. Its natural to think that this is an unnecessary complication but it has its merits that probably outweigh the cons. With default immutability the compiler guarantees that when a value is declared, it won't change; it can be accessed from various contexts and won't be updated. This not only makes code easier to read and write but also eliminates classes of bugs such as race condition and undefined behavior. These bugs rise from the following conditions:

  • Two or more pointers access the same data at the same time.
  • At least one of the pointers is being used to write to the data.
  • Access to the data is not synchronized.

Since we established that all values have exactly one owner in previous post, what are the ways multiple pointers can access the same data? The answer is borrowing; accessing data without necessarily taking ownership. Borrowing provides references which are similar to C++ pointers.

References are especially useful when passing values to functions. If all functions took ownership of arguments, we'd have to return arguments from the function so that they can be reused in the original scope. The example below illustrates how cumbersome it would be to use a function that calculate the length of a vector.

fn main()
{
  let vec = vec![16, 2, 7, 5];
  let (len, vec) = calculate_length_take(vec);
  println!("The vector {:?} has length {}", vec, len);
}

fn calculate_length_take(v : Vec<i32>) -> (usize, Vec<i32>)
{
    println!("Calculating the length of {:?}", v);
    return (v.len(), v);
}

The function calculate_length() gets a whole lot better with references. We can create a reference that borrows from vec and pass it to the function. The function can access the data without moving ownership so the vector remains valid after the function call.

fn main()
{
  let vec = vec![16, 2, 7, 5];
  let v_ref = &vec;
  println!("The vector size is {:?}", calculate_length(v_ref));
  println!("The vector is still: {:?}", vec);
}

fn calculate_length(v : &Vec<i32>) -> usize
{
    println!("Calculating the length of {:?}", v);
    return v.len();
}

As you might expect, references are immutable by default. Mutable references are allowed as long as they point to mutable data. Mutable references are by definition the perfect recipe for race conditions and bugs. To prevent that, Rust compiler enforces that that at any given scope, there is either one mutable reference or any number of immutable references to the same data.

For illustration, Let's look at C++ and Rust program that essentially do the same thing: print the smallest number in array of integers. The C++ version below is totally valid, it compiles and runs without warning.

#include <iostream>
#include <algorithm>    
#include <vector>
using namespace std;

int smallest_element(int *p)
{
    int size = sizeof(p)/ sizeof(int);
    sort(p, p+size);
    return p[0];
}

int main () {  
  int arr[] = {16, 2, 7, 5};
  cout << "The first element is: " << arr[0] << endl;
  cout << "The smallest element is: " << smallest_element(arr) << endl;
  cout << "The first element is: " << arr[0] << endl;
}

The function smallest_element looks completely harmless, it simply returns smallest element in the array, but it does so with side-effect of sorting it. Reusing arr blindly will result incorrect behavior which will be hard to debug especially if the function is imported from external library.

Let's try translating that to Rust. The snippet below is as close as we can get, you can interactively run it on Rust Playground.

fn main()
{
  let vec = vec![16, 2, 7, 5];
  let vec_ref = &vec;
  println!("The first element is: {:?}", vec_ref[0]);
  let vec_ref_2 = &vec;
  println!("The smallest element is: {:?}", smallest_element(vec_ref_2));
  println!("The first element is: {:?}", vec_ref[0]);
}

fn smallest_element(v : &Vec<i32>) -> i32
{
    v.sort();
    return v[0];
}

Does it compile?

Mutable error

No, but the error message seems straight forward. The vector sort function changes the vector, so requires it to be mutable. Let's see what happens after changing the signature of smallest_element() to take mutable reference instead.

fn smallest_element(v : &mut Vec<i32>) -> i32
{
    v.sort();
    return v[0];
}

Is the compiler satisfied?

Mismatched error

Not yet, but the error message is actionable. Since smallest_element() now takes a mutable reference, we can't pass immutable reference. Making vec_ref_2 mutable, should easily fix it.

 let vec_ref_2 = &mut vec;

Well, not quite.

Mutable ref error

Makes sense that we can't have mutable reference to immutable data. The error message recommendations have been pretty spot on so far, making vec mutable should get us closer to compilation.

  let mut vec = vec![16, 2, 7, 5];

Or not.

Immutable ref error

The problem is we are using an immutable reference to data after declaring mutable reference to it. This reference is invalid since the mutable ref may have changed the data; in this case, it did. Since vec_ref is unsafe to use, we can work around it by commenting out the last print statement.

// println!("The first element is: {:?}", vec_ref[0]);

Hooray! A working program finally 🍻.
Joy

Isn't it nice that we have caught a potentially nasty bug at compile time? Isn't it awesome that we can rely on error messages for guidance? Turns out default immutability is useful afterall. It may take some time to get used to but I'm starting to like the Rusty way.

Posted on by:

imaculate3 profile

Imaculate

@imaculate3

Engineer, Runner, Life long learner

Discussion

markdown guide
 

Thank you for that wonderful article. Very nice.

But I still have one small note. Rust is very strict on many things. This is the source of its strength. One of these constraints is code formatting. It may not please every Rustacean, but everyone uses it. Please use the tool rustfmt for example with cargo fmt to reformat your code.

 

Thanks for the feedback.