Basic Syntax of Rust
Pretty much every (procedural) programming language provides constructs to (1) define a variable, (2) define a function, and (3) control the flow of execution. If you understand those constructs for a given language, you will be able to start writing simple programs in that language. Rust is no exception, so let's take a look at how Rust provides those constructs at the very basic level first.
Variable Definitions
The following program shows how to define variables in Rust.
fn main() { let a: i32 = 1; let b: i32 = 2; println!("a + b is {}", a + b); }
But before we go further, let's note a couple of things.
main()is a function and it is the default entry point for a Rust program, similar to C/C++. If you create a newcargoproject usingcargo new, it will create amain()function insrc/main.rs.- Let's not worry about
println!()for now. All you need to know at this point is that it prints out formatted strings.
In the above program, we can make a few observations regarding variable definitions.
- There are two variable definitions in the program.
let a: i32 = 1means thatais a 32-bit integer and1is its initial value.let b: i32 = 2means thatbis a 32-bit integer and2is its initial value.
- A variable definition starts with
let, e.g.,let a: i32 = 1. - Each variable has a type, e.g.,
i32(a 32-bit integer) inlet a: i32 = 1. When you declare the type for a variable, the syntax is the variable name, followed by:, followed by the type, e.g.,a: i32. - A variable definition ends with a semicolon (
;). In Rust, all statements end with a semicolon (;). Rust makes a distinction between a statement and an expression, which we will discuss later.
Although Rust requires each variable to have a type, you don't always need to declare it because
oftentimes the Rust compiler can infer a variable's type. We can revise the above code as follows
and the Rust compiler will automatically understand the types for a and b.
fn main() { let a = 1; // No explicit type declaraion here. let b = 2; // Not here either. println!("a + b is {}", a + b); }
Keep in mind, however, that it's not always possible for the Rust compiler to infer a variable's type. If that's the case, you still need to explicitly declare the type.
The Rust book has a section on data types and you can take a look at the primitive data types available in Rust.
Function Definitions
Let's look at another piece of code that shows how to define a function.
fn main() { let a = 1; let b = 2; print_sum(a, b); } fn print_sum(x: i32, y: i32) { let sum = x + y; println!("Sum: {}", sum); }
Here also, we can make a few observations.
- There are two function definitions. One is for
main()and the other is forprint_sum(). Again,main()is the default entry point for Rust programs. - A function definition starts with
fn. print_sum()has two parameters, one isxand the other isy.x: i32means that the parameter name isxand the type isi32.y: i32means that the parameter name isyand the type isi32.
Unlike variables, parameters always need a type. Run the following code (that doesn't declare types for parameters) and see what error messages you get.
fn main() { let a = 1; let b = 2; print_sum(a, b); } fn print_sum(x, y) { let sum = x + y; println!("Sum: {}", sum); }
Typically, you want to return something as a result of calling a function. The following code shows the syntax for it.
fn main() { let a = 1; let b = 2; let s = sum(a, b); println!("a + b is {}", s); } fn sum(x: i32, y: i32) -> i32 { x + y }
There are a couple of differences that we can see.
sum()has a return type declared in its definition-> i32. It means that it returns a 32-bit integer (i32) as a return value.x + yis the return value. Notice a couple of things there.- There is no
returnkeyword. - There is no semicolon at the end.
- There is no
Not having a semicolon x + y means that x + y is an expression, and as mentioned earlier, we
will discuss it later.
You can use return in a function, either at the end or in the middle (for an early return). The
following code works exactly the same way except that it uses a return statement (notice ; at
the end) instead.
fn main() { let a = 1; let b = 2; let s = sum(a, b); println!("a + b is {}", s); } fn sum(x: i32, y: i32) -> i32 { return x + y; }
Read the Rust book on functions to understand the details about functions.
Execution Control
To control the flow of execution, Rust provides a few constructs that are similar to the ones
provided by other languages. Although they are similar, Rust's constructs for execution control are
often more powerful. There are typically two types of control you'd like---branching and looping.
Rust provides if-else if-else and match for branching and loop, while, and for for
looping.
Branching with if
Let's look at the following code, which shows how if-else works in Rust.
fn main() { let a: i32 = 1; if a == 1 { println!("a is 1"); } else if a == 2 { println!("a is 2"); } else { println!("a is something else"); } }
It looks very similar to the if-else constructs in other popular languages such as C/C++ or Java,
except that there are no parentheses for a branch condition. However, there is a notable
difference---if-else evaluates to a value. To understand what this means, let's look at the
following code.
fn main() { let a = 1; let b = if a == 1 { 2 } else if a == 2 { 1 } else { 0 }; println!("b is {}", b); }
As you can see, b gets its initial value assigned from the result of if-else. This is because
if is an expression, not a statement. An expression in Rust evaluates to a value while a statement
doesn't. In Rust, most of the constructs are expressions, e.g., if, for, a code block {}, etc.
If you add a semicolon at the end of an expression, it becomes a statement (called an expression
statement) and the value that the expression evaluates to gets ignored.
Exactly what value an if evaluates to needs some more explanation. The first thing to understand
is that a block is also an expression and it evaluates to the last expression of the block. Run the
following code and see what the result looks like.
fn main() { let a: i32 = { println!("In the block"); 1 // This is an expression. There's no `;`. }; println!("a is {}", a); }
Since a block evaluates to its last expression, a gets 1. In order to see the difference between
an expression and a statement, run the following code and see what error messages you get.
fn main() { let a: i32 = { println!("In the block"); 1; // This is a statement, not an expression due to `;`. }; println!("a is {}", a); }
If a block does not have the last expression, it gets () (sometimes referred to as the unit
type) as its type. () has a single value, which is also (), and it is used "when there's no
other meaningful value that could be returned".
Now if evaluates to the value of the block that corresponds to the correct condition. In the
following code (which is the same code from above), if evaluates to 2 because a == 1 and the
corresponding block evaluates to 2.
fn main() { let a = 1; let b = if a == 1 { 2 } else if a == 2 { 1 } else { 0 }; println!("b is {}", b); }
Branching with match
Rust provides another branching construct called match and you will probably find yourself using
it very often due to its power. However, understanding its power requires the understanding of
Rust's pattern matching ability, so we will not delve into that for now. The following code is a
revised version of the very first code we saw for if and it uses match instead of if-else if-else.
fn main() { let a: i32 = 1; match a { 1 => { // If `a` matches `1`, execute this. println!("a is 1"); } 2 => { // If `a` matches `2`, execute this. println!("a is 2"); } _ => { // `_` works as a wild card and it matches any expression println!("a is something else"); } } }
With match, you provide an expression to match (e.g., a) and list out execution options (e.g.,
1 => {}, 2 => {}, etc.). Each option is called a match arm. Unlike if, a match expression
does not have to be a boolean expression as you can see from the code above.
As mentioned earlier, match is much more powerful than if due to pattern matching, but we will
cover that later.
Loops
Rust provides three types of loops---loop, while, and for.
loop
loop is probably the most interesting loop construct, especially in conjunction with break.
loop is an infinite loop construct and if you want to break out of the loop, you need to use
break.
fn main() { let mut a: usize = 0; // Don't worry about `mut` for now. loop { println!("This is infinite..."); a += 1; if a == 10 { println!("Unless there's a break"); break; } } }
An interesting thing about loop and break is that loop is an expression and it evaluates to
what break returns.
fn main() { let mut a: usize = 0; // Don't worry about `mut` for now. let b = loop { println!("This is infinite..."); a += 1; if a == 10 { println!("Unless there's a break"); break a; } }; println!("b is {}", b); }
In the code, break returns a and loop evaluates to it. Thus, a's value is assigned to b.
while
while is almost exactly what you would expect.
fn main() { let mut a: usize = 0; // Don't worry about `mut` for now. while a < 5 { println!("a is {}", a); a += 1; } }
As with other languages, while provides a conditional loop.
for
You can use for instead of while, but you will typically use for to iterate over a collection
such as an array.
fn main() { let a = [0, 1, 2, 3, 4]; for i in a { println!("a contains {}", i); } }
In order to use for to iterate over a collection such as an array, you need to get an iterator
for the collection. In the code above, even though it looks like you are using a directly, that's
actually not the case. The Rust compiler understands that you are iterating over a and it replaces
it with a proper iterator.
You can also use range operators, .. or ..=, with for. The first one, .., is a right
exclusive range operator while ..= is a right inclusive range operator.
fn main() { let a: usize = 1; for i in 0..a { println!("Right exclusive iteration: {}", i); } for i in 0..=a { println!("Right inclusive iteration: {}", i); } }