Variable Bindings

The foundation of virtually every program is the ability to store and modify data. Rust programs are no different. Let’s start with a short example.

The basics of bindings

First, we’ll generate a new project with Cargo. Open a terminal, and navigate to the directory where you’d like to keep your projects. From there, let’s generate a new project:

$ cargo new --bin bindings
$ cd bindings

This creates a new project, ‘bindings’, and sets up our Cargo.toml and src/main.rs files. As we saw in “Hello, World!”, Cargo will generate these files and create a little ‘hello world’ program for us:

fn main() {
    println!("Hello, world!");
}

Let’s replace that program with this one:

fn main() {
    let x = 5;

    println!("The value of x is: {}", x);
}

And finally, run it:

$ cargo run
   Compiling bindings v0.1.0 (file:///projects/bindings)
     Running `target/debug/bindings`
The value of x is: 5

If you see an error instead, double check that you have copied the program exactly as written. Let’s break this down, line by line.

fn main() {

The main() function is the entry point of every Rust program. We’ll talk more about functions in the next section, but for now, all we need to know is that this is where our program begins. The opening curly brace, {, indicates the start of the function’s body.

    let x = 5;

This is our first ‘variable binding’, which we create with a ‘let statement’.

This let statement has this form:

let NAME = EXPRESSION;

A let statement first evaluates the EXPRESSION, and then binds the resulting value to NAME so that it can be referred to later in the program. In our simple example, the expression was already a value, 5, but we could achieve the same effect with:

let x = 2 + 3;

In general, let statements work with patterns; a name is a particularly humble form of pattern. Patterns are a big part of Rust, we’ll see more complex and powerful patterns as we go along.

Before we do that, though, let’s finish investigating this example. Here’s the next line:

    println!("The value of x is: {}", x);

The println! macro prints text to the screen. We can tell that it’s a macro due to the !. We won’t learn how to write macros until much later in the book, but we’ll use macros provided by the standard library throughout. Every time you see a !, remember that it signifies a macro. Macros can add new syntax to the language, and the ! is a reminder that things may look slightly unusual.

println!, specifically, has one required argument, a ‘format string’, and zero or more optional arguments. The format string can contain the special text {}. Each instance of {} corresponds to an additional argument. Here’s an example:

let x = 2 + 3;
let y = x + 5;
println!("The value of x is {}, and the value of y is {}", x, y);

You can think of {} as little crab pincers, holding the value in place. This placeholder has a number of more advanced formatting options that we’ll discuss later.

}

Finally, a closing curly brace matches up with the opening curly brace that declared the main() function, and declares its end.

This explains our output:

The value of x is: 5

We assign 5 to a binding, x, and then print it to the screen with println!.

Multiple binding

Let’s try a more complex pattern. Change our example program to this:

fn main() {
    let (x, y) = (5, 6);

    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

And run it with cargo run:

$ cargo run
   Compiling bindings v0.1.0 (file:///projects/bindings)
     Running `target/debug/bindings`
The value of x is: 5
The value of y is: 6

We’ve created two bindings with one let! Here’s our pattern:

(x, y)

And here’s the value:

(5, 6)

As you can see, the two line up visually, and so let binds 5 to x and 6 to y. We could have used two let statements as well:

fn main() {
    let x = 5;
    let y = 6;
}

In simple cases like this, two lets may be clearer, but in others, creating multiple bindings at once is nice. As we become more proficient in Rust, we’ll figure out which style is better, but it’s mostly a judgement call.

Type annotations

You may have noticed that we didn’t declare the type of x or y in our previous examples. Rust is a statically typed language, which means that at compile time, we must know the types of all bindings. But annotating every single binding with a type can feel like busywork, and make code noisy. To solve this issue, Rust uses ‘type inference’, meaning that it attempts to infer the types of your bindings.

The primary way that the type is inferred is by looking at how it is used. Let’s look at the example again:

fn main() {
    let x = 5;
}

When we bind x to 5, the compiler knows that x should be a numeric type. Without any other information, it defaults to i32, a thirty-two bit integer type. We’ll talk more about Rust’s basic types in section 3.3.

Here’s what a let statement with a ‘type annotation’ looks like:

fn main() {
    let x: i32 = 5;
}

We can add a colon, followed by the type name. Here’s the structure of a let statement with a type annotation:

let PATTERN: TYPE = VALUE;

Note that the colon and the TYPE go after the PATTERN, not in the pattern itself. As an example, here’s our more complex pattern with two bindings:

fn main() {
    let (x, y): (i32, i32) = (5, 6);
}

Just like we match up the VALUE with the PATTERN, we match up the TYPE with the PATTERN.

Delayed Initialization

We do not have to provide bindings with an initial value, and can assign it later. Try this program:

fn main() {
    let x;

    x = 5;

    println!("The value of x is: {}", x);
}

And run it with cargo run:

$ cargo run
   Compiling bindings v0.1.0 (file:///projects/bindings)
     Running `target/debug/bindings`
The value of x is: 5

It’s all good. This raises a question, though: what if we try to print out a binding before we declare a value? Here’s a program that demonstrates this question:

fn main() {
    let x;

    println!("The value of x is: {}", x);

    x = 5;
}

We can find out the answer with cargo run:

   Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:4:39: 4:40 error: use of possibly uninitialized variable: `x` [E0381]
src/main.rs:4     println!(“The value of x is: {}”, x);
                                                    ^
<std macros>:2:25: 2:56 note: in this expansion of format_args!
<std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>)
src/main.rs:4:5: 4:42 note: in this expansion of println! (defined in <std macros>)
src/main.rs:4:39: 4:40 help: run `rustc --explain E0381` to see a detailed explanation
error: aborting due to previous error
Could not compile `bindings`.

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

An error! The compiler won’t let us write a program like this. This is our first example of the compiler helping us find an error in our program. Different programming languages have different ways of approaching this problem. Some languages always initialize values with some sort of default. Other languages leave the value uninitialized, and make no promises about what happens if you try to use something before initialization. Rust chooses something else: error and force the programmer to explain what they want. We must do some sort of initialization before we can use x.

Extended error explanations

There’s one more interesting part of this error message:

src/main.rs:4:39: 4:40 help: run `rustc --explain E0381` to see a detailed explanation

We can see an extended explanation by passing the --explain flag to rustc. Not every error has a longer explanation, but many of them do. These extended explanations try to show off common ways that the error occurs, and common solutions to the issue. Here’s E0381:

$ rustc --explain E0381
It is not allowed to use or capture an uninitialized variable. For example:

fn main() {
    let x: i32;
    let y = x; // error, use of possibly uninitialized variable

To fix this, ensure that any declared variables are initialized before being
used.

These explanations can really help if you’re stuck on an error. The compiler is your friend, and is here to help.

Mutable bindings

What about changing the value of a binding? Here’s another sample program that asks this question:

fn main() {
    let x = 5;

    x = 6;

    println!("The value of x is: {}", x);
}

cargo run has the answer for us:

$ cargo run
   Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:4:5: 4:10 error: re-assignment of immutable variable `x` [E0384]
src/main.rs:4     x = 6;
                  ^~~~~
src/main.rs:4:5: 4:10 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:2:9: 2:10 note: prior assignment occurs here
src/main.rs:2     let x = 5;
                      ^

The error mentions re-assigment of immutable variable. That’s right: bindings are immutable. But they’re only immutable by default. In a pattern, when we’re creating a new name, we can add mut in front to make the binding a mutable one. Here’s an example:

fn main() {
    let mut x = 5;

    println!("The value of x is: {}", x);

    x = 6;

    println!("The value of x is: {}", x);
}

Running this, we get:

$ cargo run
   Compiling bindings v0.1.0 (file:///projects/bindings)
     Running `target/debug/bindings`
The value of x is: 5
The value of x is: 6

We can now change the value that x binds to. Note that the syntax is not let mut exactly; it’s using mut in a pattern. This becomes more obvious with our () pattern:

fn main() {
    let (mut x, y) = (5, 6);

    x = 7;
    y = 8;
}

The compiler will complain about this program:

$ cargo build
   Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:5:5: 5:10 error: re-assignment of immutable variable `y` [E0384]
src/main.rs:5     y = 8;
                  ^~~~~
src/main.rs:5:5: 5:10 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:2:17: 2:18 note: prior assignment occurs here
src/main.rs:2     let (mut x, y) = (5, 6);
                              ^

It’s fine with re-assigning x, but not y. The mut only applies to the name that follows it, not the whole pattern.

Reassignment, not mutation

There is one subtlety we haven’t covered yet: mut allows you to mutate the binding, but not what the binding binds to. In other words:

fn main() {
    let mut x = 5;

    x = 6;
}

This is not changing the value that x is bound to, but creating a new value, 6, and changing the binding to bind to it instead. It’s a subtle but important difference. Well, for now, it does not make a lot of difference, but when our programs get more complex, it will. Specifically, passing arguments to functions will illustrate the difference. We’ll talk about that in the next section, when we discuss functions.

Scope

Variable bindings have a ‘scope’ in which they’re valid. That scope begins from the point at which the binding is declared, and ends at the end of the next block of code. We can only access bindings which are ‘in scope’. We cannot access them ‘before they come into scope’ or ‘after they go out of scope’. Here’s an example:

fn main() {
    println!("x is not yet in scope");

    let x = 5;
    println!("x is now in scope");

    println!("In real code, we’d now do a bunch of work."); 
    
    println!("x will go out of scope now! The next curly brace is ending the main function.");
}

We can create arbitrary scopes through the use of { and }:

fn main() {
    println!("x is not yet in scope");

    let x = 5;
    println!("x is now in scope");

    println!("Let’s start a new scope!");

    {
        let y = 5;
        println!("y is now in scope");
        println!("x is also still in scope");

        println!("y will go out of scope now!");
        println!("The next curly brace is ending the scope we started.");
    }

    println!("x is still in scope, but y is now out of scope and is not usable");
    
    println!("x will go out of scope now! The next curly brace is ending the main function.");
}

What bindings are in and out of scope will become much more important later, once we learn about ‘references’ and ‘traits’.

Shadowing

A final thing about bindings: they can ‘shadow’ previous bindings with the same name. Here’s a sample program:

fn main() {
    let x = 5;
    let x = 6;

    println!("The value of x is: {}", x);
}  

Running it, we can see the shadowing in action:

src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variables)] on by default
src/main.rs:2     let x = 5;
                      ^
     Running `target/debug/bindings`
The value of x is: 6

There are two interesting things in this output. First, Rust will compile and run this program, no problem. And as we can see, the value of x is 6. But we didn’t declare x as mutable. Instead, we declared a new binding that is also named x, and gave it a new value. The older value that we bound x to is inaccessible as soon as the new x is declared. This can be useful if you’d like to perform a few transformations on a value, and leave it immutable. For example:

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;

    println!("The value of x is: {}", x);
}

This will print:

   Compiling bindings v0.1.0 (file:///projects/bindings)
     Running `target/debug/bindings`
The value of x is: 12

This lets us modify x, but not deal with mutation. This is nice because we know that the compiler will let us know if we try to modify it later. Let’s assume that after we calculate 12, we don’t want to modify x again. If we had written this program in a mutable style, like this:

fn main() {
    let mut x = 5;
    x = x + 1;
    x = x * 2;

    println!("The value of x is: {}", x);

    x = 15;

    println!("The value of x is: {}", x);
}

Rust is happy to let us mutate it again, to 15. A similar program in our immutable style will let us know about that accidental mutation, however:

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;

    println!("The value of x is: {}", x);

    x = 15;

    println!("The value of x is: {}", x);
}

If we try to compile, we get an error:

$ cargo build
   Compiling bindings v0.1.0 (file:///projects/bindings)
src/main.rs:8:5: 8:11 error: re-assignment of immutable variable `x` [E0384]
src/main.rs:8     x = 15;
                  ^~~~~~
src/main.rs:8:5: 8:11 help: run `rustc --explain E0384` to see a detailed explanation
src/main.rs:4:9: 4:10 note: prior assignment occurs here
src/main.rs:4     let x = x * 2;
                      ^
error: aborting due to previous error
Could not compile `bindings`.

Exactly what we wanted.

Shadowing can take some time to get used to, but it’s very powerful, and works well with immutability.

There was one more thing we should talk about in the output from compiling our initial program. It’s this part:

src/main.rs:2:9: 2:10 warning: unused variable: `x`, #[warn(unused_variables)] on by default

Here’s the two lines of relevant code:

let x = 5;
let x = 6;

Rust knows that we shadowed x, but we never ended up using the initial value. This isn’t wrong, exactly, it just may not have been what we wanted. In this case, the compiler issues a ‘warning’, but still compiles our program. The #[warn(unused_variables)] syntax is called an ‘attribute’, which we’ll discuss in a later section. More specifically, a warning like this is called a ‘lint’, which is an old term for the bits of sheep’s wool that you wouldn’t want to put in cloth. Similarly, this lint is telling us that we may have an extra bit of code we don’t need. Our program would work just fine without it. It’s worth listening to these warnings, and fixing the problems they point out. They can be signs of a larger problem. In this case, we may not have realized that we were shadowing x.

Shadowing and scopes

Like any binding, a binding that shadows another binding will go away at the end of a scope. Here’s an example program:

fn main() {
    let x = 5;

    println!("Before shadowing, x is: {}", x);

    {
        let x = 6;

        println!("Now that x is shadowed, x is: {}", x);
    }

    println!("After shadowing, x is: {}", x);
}

If we run this example, we can see the shadow appear and disappear:

$ cargo run
   Compiling bindings v0.1.0 (file:///projects/bindings)
     Running `target/debug/bindings`
Before shadowing, x is: 5
Now that x is shadowed, x is: 6
After shadowing, x is: 5