Rust's take on if is not particularly complex, but it's much more like the if you'll find in a dynamically typed language than in a more traditional systems language. So let's talk about it, to make sure you grasp the nuances.

if is a specific form of a more general concept, the branch. The name comes from a branch in a tree: a decision point, where depending on a choice, multiple paths can be taken.

In the case of if, there is one choice that leads down two paths:

let x = 5;

if x == 5 {
    println!("x is five!");

If we changed the value of x to something else, this line would not print. More specifically, if the expression after the if evaluates to true, then the block is executed. If it's false, then it is not.

If you want something to happen in the false case, use an else:

let x = 5;

if x == 5 {
    println!("x is five!");
} else {
    println!("x is not five :(");

This is all pretty standard. However, you can also do this:

let x = 5;

let y = if x == 5 {
} else {
}; // y: i32

Which we can (and probably should) write like this:

let x = 5;

let y = if x == 5 { 10 } else { 15 }; // y: i32

This reveals two interesting things about Rust: it is an expression-based language, and semicolons are different from semicolons in other 'curly brace and semicolon'-based languages. These two things are related.

Expressions vs. Statements

Rust is primarily an expression based language. There are only two kinds of statements, and everything else is an expression.

So what's the difference? Expressions return a value, and statements do not. In many languages, if is a statement, and therefore, let x = if ... would make no sense. But in Rust, if is an expression, which means that it returns a value. We can then use this value to initialize the binding.

Speaking of which, bindings are a kind of the first of Rust's two statements. The proper name is a declaration statement. So far, let is the only kind of declaration statement we've seen. Let's talk about that some more.

In some languages, variable bindings can be written as expressions, not just statements. Like Ruby:

x = y = 5

In Rust, however, using let to introduce a binding is not an expression. The following will produce a compile-time error:

let x = (let y = 5); // expected identifier, found keyword `let`

The compiler is telling us here that it was expecting to see the beginning of an expression, and a let can only begin a statement, not an expression.

Note that assigning to an already-bound variable (e.g. y = 5) is still an expression, although its value is not particularly useful. Unlike C, where an assignment evaluates to the assigned value (e.g. 5 in the previous example), in Rust the value of an assignment is the unit type () (which we'll cover later).

The second kind of statement in Rust is the expression statement. Its purpose is to turn any expression into a statement. In practical terms, Rust's grammar expects statements to follow other statements. This means that you use semicolons to separate expressions from each other. This means that Rust looks a lot like most other languages that require you to use semicolons at the end of every line, and you will see semicolons at the end of almost every line of Rust code you see.

What is this exception that makes us say "almost"? You saw it already, in this code:

let x = 5;

let y: i32 = if x == 5 { 10 } else { 15 };

Note that I've added the type annotation to y, to specify explicitly that I want y to be an integer.

This is not the same as this, which won't compile:

let x = 5;

let y: i32 = if x == 5 { 10; } else { 15; };

Note the semicolons after the 10 and 15. Rust will give us the following error:

error: mismatched types: expected `i32`, found `()` (expected i32, found ())

We expected an integer, but we got (). () is pronounced unit, and is a special type in Rust's type system. In Rust, () is not a valid value for a variable of type i32. It's only a valid value for variables of the type (), which aren't very useful. Remember how we said statements don't return a value? Well, that's the purpose of unit in this case. The semicolon turns any expression into a statement by throwing away its value and returning unit instead.

There's one more time in which you won't see a semicolon at the end of a line of Rust code. For that, we'll need our next concept: functions.