Create your own
Lesson illustration

Variable Bindings: Mutable vs. Immutable

Hello! Welcome to the first hands-on coding lesson in our journey to becoming a Solana developer. Now that your development environment is set up, we can start diving into the Rust programming language.

This lesson kicks off our module on Rust fundamentals. Our goal today is to master one of the most basic but crucial concepts in Rust: how to declare and use variables. You'll learn about Rust's unique approach to variable bindings, specifically the concepts of immutability and mutability.

Given your extensive background in JavaScript, you're already very familiar with variables. However, Rust handles them differently. Where JavaScript uses let for mutable variables and const for immutable ones, Rust has its own philosophy that is central to its promise of safety and performance—two features that are paramount in blockchain development. Understanding these rules is the first step toward writing secure and efficient Solana programs.

1. Immutability: The Default in Rust

In Rust, you declare a variable using the let keyword, just like in modern JavaScript. However, there's a critical difference: variables in Rust are immutable by default. This means once a value is assigned to a variable, you cannot change it.

Let's see what this means in practice. In your IDE, inside the main function of a new Cargo project, you can write:

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    
    // This next line will cause an error!
    // x = 6; 
    // println!("The value of x is: {}", x);
}

If you uncomment the last two lines and try to run this with cargo run, the compiler will stop you with an error: cannot assign twice to immutable variable 'x'.

This might seem restrictive, but it's a deliberate design choice. This compile-time check prevents a whole class of bugs where a value is changed unexpectedly, which is especially dangerous in a blockchain context where program correctness is tied to financial security.

To get a solid grasp of this foundational concept, please read the first part of the official Rust documentation on variables.

Variables and Mutability - The Rust Programming Language

This is a chapter from 'The Rust Programming Language' book, the definitive guide to Rust. It explains why Rust variables are immutable by default and illustrates the concept with a clear example and compiler error.

Please read the text from the beginning of the page under the Variables and Mutability heading down to the section titled 'Constants'. In the introduction to default immutability, focus on understanding the rationale behind immutability and how the compiler enforces it.

2. Introducing Mutability with mut

Of course, we often need to change a variable's value. To do this, Rust requires you to be explicit. You can make a variable mutable by adding the mut keyword before the variable name.

Let's fix our previous example:

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    
    x = 6; // This is now allowed!
    println!("The value of x is: {}", x);
}

Now, the program compiles and runs successfully, printing 5 and then 6. By using let mut, you are explicitly telling the Rust compiler (and anyone reading your code) that this variable's value is expected to change. This intentionality makes the code easier to reason about.

For a dynamic walkthrough of both immutable and mutable variables, the following video provides a great summary.

Rust Tutorial #3 - Variables, Constants and Shadowing

This video from 'Tech With Tim' demonstrates the concepts of immutable and mutable variables in a clear, step-by-step manner.

A little over a minute into the video, watch the sections 'Declaring Immutable Variables' and, immediately following, 'Declaring Mutable Variables'. Pay attention to the live coding and the compiler's feedback.

Test your understanding!

You are writing a program to count the number of login attempts. The following code is supposed to increment the attempts counter, but it fails to compile.

fn main() {
    let attempts = 0;
    println!("Initial attempts: {}", attempts);
    
    // User tries to log in...
    attempts = 1;
    println!("Attempts after failure: {}", attempts);
}

How would you fix this code so that it compiles and runs correctly?

Show answer

You need to make the attempts variable mutable by adding the mut keyword.

fn main() {
    let mut attempts = 0; // Added 'mut' here
    println!("Initial attempts: {}", attempts);
    
    // User tries to log in...
    attempts = 1;
    println!("Attempts after failure: {}", attempts);
}

3. Shadowing: A Different Kind of Change

Rust has another powerful concept called shadowing. It looks similar to reassignment, but it's fundamentally different. You can declare a new variable with the same name as a previous variable. This new variable "shadows" the previous one.

fn main() {
    let x = 5;

    // Here, we shadow the first 'x' with a new 'x'.
    let x = x + 1; 

    {
        // Inside this new scope, we shadow 'x' again.
        let x = x * 2;
        println!("The value of x in the inner scope is: {}", x); // Prints 12
    }

    // When the scope ends, the inner shadowing is over. We're back to the second 'x'.
    println!("The value of x is: {}", x); // Prints 6
}

So, what's the difference between using mut and shadowing?

  1. Immutability: With shadowing, you use let again, so the new variable is itself immutable (unless you use let mut). This allows you to perform some transformations on a value and then have it be immutable afterward.
  2. Type Change: This is a key difference. When you use mut, you can only change the value, not the variable's type. Shadowing, because it creates an entirely new variable, allows you to change the type while reusing the name.

This second point is particularly useful. In JavaScript, you might reassign a variable to hold a value of a different type. In Rust, you can achieve a similar convenience through shadowing, but in a type-safe way.

fn main() {
    // `config_value` is a string
    let config_value = "true"; 
    
    // We shadow `config_value` with a new variable of type boolean
    let config_value = config_value.parse::<bool>().unwrap_or(false); 

    println!("The configuration is set to: {}", config_value);
}

Attempting this with mut would result in a compiler error.

To explore shadowing in more detail, please review the final sections of the resources we've been using.

Variables and Mutability - The Rust Programming Language

The Rust Book provides a precise explanation of shadowing, contrasting it with mutability and highlighting its ability to change a variable's type.

In the section titled 'Shadowing', read about variable shadowing. Focus on the examples that show how shadowing differs from mut, especially the example that changes the variable's type.

Rust Tutorial #3 - Variables, Constants and Shadowing

The 'Tech With Tim' video also covers shadowing, demonstrating how scope affects shadowed variables and how you can change a variable's type.

About six and a half minutes in, watch the section on Variable Shadowing.

Test your understanding!

A user provides their age as a string. Your task is to process this input. You first receive it as a string, then need to convert it into a number for calculations.

fn main() {
    // Code block 1
    let age = "30";
    println!("Age as string: {}", age);
    
    // Code block 2 (Choose one implementation)
    // ???

    println!("Age as number: {}", age);
}

Which of the following implementations should you use for // Code block 2 and why?

A)

let mut age = age.parse::<u32>().unwrap();

B)

let age = age.parse::<u32>().unwrap();

C)

age = age.parse::<u32>().unwrap();
Show answer

The correct answer is B.

Here's why:

  • We are changing the type of the age variable from a string slice (&str) to an integer (u32).
  • mut only allows changing the value of a variable, not its type. So, option C would cause a compiler error.
  • Option A is incorrect because it mixes mut with shadowing in a confusing way and is unnecessary if you don't need to change the age number later.
  • Option B correctly uses shadowing (let age = ...) to create a new, immutable variable named age with the correct numeric type. This is the idiomatic Rust way to handle this kind of type conversion.

Conclusion

Congratulations on completing your first lesson on Rust! You've learned the fundamental rules that govern variables, which are a cornerstone of the language's safety and reliability.

Here are the key takeaways:

  • Immutable by Default: Variables declared with let cannot be changed. This is a core safety feature of Rust.
  • Explicit Mutability: To create a variable that can be changed, you must use the let mut keyword.
  • Shadowing for Transformation: You can declare a new variable with the same name as an old one using let. This is called shadowing and is useful for transforming a value, especially if it involves changing its type.

These principles of immutability and explicit mutability are not just abstract rules; they are what enable Rust to prevent entire categories of bugs at compile time. In the world of Solana, where a single bug can have significant financial consequences, leveraging the Rust compiler's strictness is your first line of defense.

In our next lesson, we will build directly on this foundation by exploring the different data types that variables can hold in Rust, such as integers, booleans, tuples, and arrays.

Can't find a good explanation? Sign up and we'll make it for you

Sign up