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 newcargo
project 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 = 1
means thata
is a 32-bit integer and1
is its initial value.let b: i32 = 2
means thatb
is a 32-bit integer and2
is 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 isx
and the other isy
.x: i32
means that the parameter name isx
and the type isi32
.y: i32
means that the parameter name isy
and 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 + y
is the return value. Notice a couple of things there.- There is no
return
keyword. - 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); } }