Manipulating variables is one of the most powerful features of a programming language . A variable is a name that refers to a value. Let’s repeat that sentence: A variable is a name that refers to a value.

That means that it points to a value. Other variables are free to point at the same value if you want them to do so.

Rust rests on a couple of principles that may seem odd and even inconvenient at times. Especially if you’re used to another programming language. That inconvenience will pay huge dividends. It will make your Rust programs great, it will even make you write better code in your old language!

The first of these principles is that every value is immutable, i.e., the value itself cannot change. But we can change what value our variable points at. When we do, we mutate our variable. Let’s see how that works.

Assignment statements

An assignment statement creates a new variable and points it at a value:

let message = "And now for something completely different";
let n = 17;
let pi = 3.1415926535897932;

This example makes three assignments. The first creates a new variable named message and makes it point to a string literal. The second points the variable n at the integer 17. The third points the variable pi to the (approximate) decimal or floating point value of π.

Note that each assignment starts with the word let. This is a special word that we use when we create, or declare a variable for the first time. It tells our compiler that we are about to create a new variable.

Variable types

Variables can be of many kinds. Whole numbers (integers). Decimal numbers (floating point). Strings, string slices, arrays and a lot of other types.

Rust is a strongly typed language. This means that we always need to declare what type every variable has. We also need to declare what type the result of every function is. Sometimes the compiler will help us by inferring the type, when we haven’t told it what type we want. In the first couple of posts we’ll be using something called primitive types only. With one exception, the String type.

Primitive types in Rust

There are a number “built-in” types in Rust. We call them Rusts primitive types. Most are number types:

Integers

Integers come in two variants. Either signed (meaning they can have negative values), or unsigned (only positive values).

They can also have different lengths, or sizes. The length is the number of bits (binary digits, i.e. ones and zeros) the number contains.

To declare that a number is a signed integer you can put the letter i after it, followed by the number of bits, like this:

let a = 25i32;

This will make sure that the number that a points to is a 32-bit signed integer with the value of 25.

If we want an unsigned integer of the same length, we write:

let a = 25u32;

There is a fixed number of lengths you can use for integer numbers, and the lengths are: 8, 16, 32 and 64.

There are also two special sizes for integers, usize for unsigned integers. And isize, for signed integers. These pick the largest size available for addresses in your operating system, either 32 or 64. If you have a 32-bit operating system, usize and isize will both be 32 bits, otherwise they will be 64 bits.

Decimal numbers

For decimal numbers, i.e. floating point, there are only two lengths available, 32 and 64.

If we want the number in the previous example to be a floating point number, we would declare it like this:

let a = 25.0;

We don’t know what length that floating point number will have though. But the compiler infers that it is a floating point number since it has a decimal point.

If we want to specify the length, we do it like we did with integers:

let a = 25.0f32;

You can probably guess what we would write to get a 64-bit floating point number instead…

We’ll introduce other types as we need them.

Variable names

Programmers generally choose names for their variables that are meaningful. The name shows the intended use of the variable. Make a habit of that, even if that means that your variable names get long. Clarity beats brevity every time.

Don’t confuse the reader of your code! In a couple of months that reader might be you, and you’ve likely forgotten the details of your program. In that situation, clear and descriptive variable names will help a lot.

Variable names can be as long as you like. They can contain both letters and numbers, but they can’t begin with a number. It is legal to use uppercase letters, but it is conventional to use only lower case for variable names.

Good variable names can reduce the need for comments in the code (we’ll get to comments in a short while). But long names can make complex expressions hard to read, so there is a tradeoff.

The underscore character,_, can appear in a name. It is often used in names with many words. Like user_name or airspeed_of_unladen_swallow. If you give a variable an illegal name, you get a syntax error. If you for example try to create a variable with a name that starts with a number:

fn main() {
    let 2_illegal_name = 42;
    println!("{}", 2_illegal_name);
}

Your terminal explodes with complaints from the compiler:

error: invalid suffix `illegal_name` for integer literal
 --> src/main.rs:2:9
  |
2 |     let 2_illegal_name = 42;
  |         ^^^^^^^^^^^^^^ invalid suffix `illegal_name`
  |
  = help: the suffix must be one of the integral types (`u32`, `isize`, etc)

error: invalid suffix `illegal_name` for integer literal
 --> src/main.rs:3:20
  |
3 |     println!("{}", 2_illegal_name);
  |                    ^^^^^^^^^^^^^^ invalid suffix `illegal_name`
  |
  = help: the suffix must be one of the integral types (`u32`, `isize`, etc)

error: aborting due to 2 previous errors

error: could not compile `c02`.

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

In fact, the compiler seems to have interpreted illegal_name as a suffix to the integer value of 2. This is not at all what we wanted.

If we put the number anywhere else in the name it’s ok though, but not at the very beginning:

fn main() {
    let legal_name_2 = 42;
    let _another_2_legal_name = 43;
    println!("{}", legal_name_2);
}

This compiles just fine. So, there are basically three rules:

  1. You can compose he name of a variable from letters, digits, and the underscore character.
  2. It must begin with either a letter or an underscore.
  3. Upper and lowercase letters are distinct because Rust is case-sensitive.

There is one more thing we need to do: we have to think about mutability. Do we intend to change what value the variable should point at? Let’s say we declare a variable user_name and first give it the value ”Bob”. Later we decide to change it to point at the more formal string ”Robert” instead:

fn main() {
    let user_name = "Bob";
    // lots of code here doing other things
    // then we decide to change from "Bob"
    // to "Robert"
    user_name = "Robert";
    println!("{}", user_name);
}

The compiler clearly states it doesn’t approve:

warning: value assigned to `user_name` is never read
 --> src/main.rs:2:9
  |
2 |     let user_name = "Bob";
  |         ^^^^^^^^^
  |
  = note: `#[warn(unused_assignments)]` on by default
  = help: maybe it is overwritten before being read?

error[E0384]: cannot assign twice to immutable variable `user_name`
 --> src/main.rs:6:5
  |
2 |     let user_name = "Bob";
  |         ---------
  |         |
  |         first assignment to `user_name`
  |         help: make this binding mutable: `mut user_name`
...
6 |     user_name = "Robert";
  |     ^^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable

error: aborting due to previous error

For more information about this error, try `rustc --explain E0384`.
error: could not compile `c02`.

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

The first thing we see is a warning that user_nameis never read anywhere. That means we don’t actually use it to do anything. This is not a real problem, but it does make the code less clear to read if we clutter it with unused variables. That is not a showstopper though, it would still work.

What is a showstopper is the error[E0384] that appears later. This is a real error, and means that the compiler refuses to continue. We will not get an executable program.

That’s the bad news.

The good news is that it tells us how to fix it.

Part of the error message is

help: make this binding mutable: `mut user_name`

If we do that, we get a different result:

fn main() {
    let mut user_name = "Bob";
    // lots of code here doing other things
    // then we decide to change from "Bob"
    // to "Robert"
    user_name = "Robert";
    println!("{}", user_name);
}

Now we see this in the terminal instead:

warning: value assigned to `user_name` is never read
 --> src/main.rs:2:13
  |
2 |     let mut user_name = "Bob";
  |             ^^^^^^^^^
  |
  = note: `#[warn(unused_assignments)]` on by default
  = help: maybe it is overwritten before being read?

    Finished dev [unoptimized + debuginfo] target(s) in 0.36s

The most important part of that message is the last line. It says it finished, which means it worked, and we now have a working program!

That warning is rather pesky though, let’s get rid of it. When we assign variable names, we can tell the compiler not to check if we use them later or not. We start the name with an underscore, like so: _user_name. Let’s do that:

fn main() {
    let mut _user_name = "Bob";
    // lots of code here doing other things
    // then we decide to change from "Bob"
    // to "Robert"
    _user_name = "Robert";
    println!("{}", _user_name);
}

Now all we get is:

    Finished dev [unoptimized + debuginfo] target(s) in 4.54s
     Running `target/debug/c02`
Robert

Perfect!

Expressions and statements

An expression is a combination of values, variables, and operators.

A statement is a unit of code that has an effect, like creating a variable or displaying a value.

The first line is an assignment statement that points _user_name to a value of ”Bob”. The last line is a print statement that displays the value of _user_name.

Order of operations

The value of an expression with more than one operator, depends on the order of the operators . For mathematical operators, Rust follows mathematical convention. The acronym PEMDAS is a useful way to remember the rules:

  • Parentheses have the highest precedence. You can use them to force Rust to use the operators in the order you want. Since Rust evaluates expressions in parentheses first, 2(3-1) is 4, and (1+1)(3+1) is 8.

You can also use parentheses to make an expression easier to read. This works even if it doesn’t change the result. The compiler will give a warning if you do and suggest that you remove them, but a warning is not an error.

  • Exponentiation is special in Rust as it has no exponentiation operator. Rust uses a function called pow instead. We’re gonna leave that for now, and come back to it in the next chapter, when we look at math functions.

  • Multiplication and Division have higher precedence than Addition and Subtraction. So 2*3-1 is 5, not 4, and 6+4/2 is 8, not 5.

  • Rust will check operators with the same precedence from left to right. In the expression degrees/2*pi, the division happens first. Then Rust multiplies the result with pi. To divide by 2pi, you use parentheses, like so: degrees/(2*pi).

I don’t work very hard to remember the precedence of operators. If I can’t tell by looking at the expression, I use parentheses to make it obvious.

String operations

You can’t perform mathematical operations on strings. Not even if the strings look like numbers. Instead we use functions to manipulate variables that point to strings. More about that later.

Comments

As programs get bigger and more complicated, they become more difficult to read. Formal languages are dense, and it is often difficult to look at a piece of code and figure out what it is doing, or why.

For this reason, it is a good idea to add notes to your programs to explain in natural language what they are doing. We call these notes comments.

There are three types of comments, block comments, line comments and documentation comments.

Using block comments is generally discouraged. The Rust documentation says you should avoid them, so I won’t use them in this blog. The reason is that it is easy to miss where they begin and end. They start with /* and end with */ if you decide to use them anyway…

Line comments start with //, and you need to repeat the // on every line:

fn main() {
    let mut user_name = "Bob";
    // lots of code here doing other things
    // then we decide to change from "Bob"
    // to "Robert"
    user_name = "Robert";
    println!("{}", user_name);
}

You can also add a comment to the end of a line, like on the last line in this example:

fn main() {
    let mut user_name = "Bob";
    // lots of code here doing other things
    // then we decide to change from "Bob"
    // to "Robert"
    user_name = "Robert";
    println!("{}", user_name); // print to stdout
}

The compiler ignores everything from the // to the end of the line – it has no effect on the execution of the program.

Doc comments start with /// on every line, and they are a special kind of comment. They can be picked up by a utility called cargo that you will install later if you haven’t already. The command cargo doc in the terminal will build documentation for your program. The documentation will end up in a directory named target/doc.

You can even include test code in your doc comments! That code will become part of your documentation as expected. You can also run it with the command rustdoc --test file.rs, where file.rs is the file with the tests.

Debugging

Three kinds of errors can occur in a program. Syntax errors, runtime errors, and semantic errors. It is useful to distinguish between them to make it easier to track them down.

Syntax error:
“Syntax” refers to the structure of a program and the rules about that structure. For example, parentheses have to come in matching pairs, so `(8 + 1)` is legal, but `8)` is a **syntax error**. If there is a syntax error in your program, the compiler displays an error message and quits. It does so without compiling, and you will not have a program to run. In the first few weeks of your programming career, you will spend a lot of time tracking down syntax errors. As you gain experience, you will make fewer errors of this kind and find them faster.
Runtime error:
The second type of error is a runtime error. It's called a runtime error because it does not appear until the program is running. Runtime errors are rare in the simple programs you will see in the first few chapters. It might be a while before you encounter one.
Semantic error:
The third type of error is “semantic”, which means related to meaning. If there is a semantic error in your program, it will run without generating error messages. But it will not do the right thing. It will do something else. It will do what you told it to do. Identifying semantic errors can be tricky. It requires you to work backward by looking at the output of the program and trying to figure out what it is doing.

About the author

For the last three decades, I've worked with a variety of technologies. I'm currently focused on fullstack development. On my day to day job, I'm a senior teacher and course developer at a higher vocational school in Malmoe, Sweden. I'm also an occasional tech speaker and a mentor. Do you want to know more? Visit my website!