Introduction
Rust is a systems programming language that is known for its speed, safety, and concurrency features. In this tutorial, we're going to take a deep dive into some essential aspects of Rust’s syntax and structure. By the end, you’ll have a good understanding of how to correctly assemble a Rust program and follow best practices for writing clean and efficient code.
We will explore the following concepts:
- Pairing and Nesting Symbols
- Proper Layout and Formatting
- Comments
- Expressions, Values, and Types
- Variables
- The Difference Between Effects and Results
- Blocks and Statements
Let’s get started!
Pairing and Nesting
In Rust, many symbols like {}
and ()
come in pairs. It's important to ensure that these pairs are nested correctly within each other. For example, in the code below:
fn main() {
println!("Hello, world!");
}
The curly braces {}
enclose the body of the main
function. The parentheses ()
are paired around the arguments passed to the println!
macro. If any of these pairs were left open or improperly nested, Rust would throw an error.
Now compare that with this incorrect version:
fn main(){println!("Hello, world!"
This code will fail because one of the parentheses and the closing curly brace are missing. Ensuring correct pairing and nesting is critical in Rust.
Layout
Let's look at these two snippets of code:
Code 1:
fn main() {
println!("Hello, world!");
{
let name = "Kim";
println!("Nice to meet you, {}!", name);
}
println!("Have a great day");
}
Code 2:
fn main(){println!("Hello, world!"
)
;{
let name = "Kim"; println!(
"Nice to meet you, {}!", name
);
} println!("Have a great day")
; }
Both snippets run the same, but Code 1 follows Rust's recommended formatting practices, making it easier to read and maintain. Code 2, though functionally correct, is difficult to interpret because of inconsistent spacing and indentation. Following best practices such as proper indentation and spacing between elements not only makes the code more readable for other programmers but also easier to maintain in the long run.
You can use the Rust Playground, a website for writing Rust code.
Using a tool like Rustfmt, you can automatically reformat Code 2 to look like Code 1, ensuring clarity and consistency. It's a good habit to regularly format your code to keep it clean, especially in larger projects.
Comments in Rust
Continuing with the idea that "the computer doesn't care," there's a feature called comments. Comments allow you to include messages in your code for other programmers to read. Sometimes, those "other programmers" will be your future self—six months down the line—when you’ve forgotten everything about the code. Writing clear, helpful comments can make a big difference for both you and others working on the code.
In Rust, there are two ways to write comments:
-
Single-line comments: Use two forward slashes
//
. Everything after//
on that line is ignored by the compiler. Here's an example:fn main() { // This line is completely ignored by the compiler println!("Hello, world!"); // This prints a message // All done, bye! }
-
Block comments: Use
/*
to start the comment and*/
to end it. This type of comment can be placed anywhere in your code, even in the middle of a line. It’s also useful for multi-line comments. The downside is that it's more verbose compared to single-line comments (//
). Here’s an example:fn main(/* You can even comment here! */) { /* This whole line is ignored by the compiler */ println!("Hello, world!" /* sneaky comment */); /* All done, bye! I had a great time programming with you. */ }
Feel free to use whichever type of comment best fits the situation.
Commenting Out Code
A common practice when debugging or testing is to "comment out" lines of code. This means turning a line of code into a comment so it’s ignored by the compiler without deleting it. For example:
fn main() {
println!("Hello, world!");
println!("I want to comment this out");
}
If you want to disable the second println!
, you can simply comment it out like this:
fn main() {
println!("Hello, world!");
// println!("I want to comment this out");
}
Exercise
Try commenting out the first and third println!
calls in the code below:
fn main() {
println!("Hello, world!");
println!("Still alive!");
println!("I'm tired, good night!");
}
Once you’ve done that, your code should look something like this:
fn main() {
// println!("Hello, world!");
println!("Still alive!");
// println!("I'm tired, good night!");
}
Comments are a simple yet powerful tool that can greatly improve the readability and maintainability of your code. I'll be using comments throughout the examples to provide additional information and hints. Make sure to get in the habit of using them effectively!
Expressions, Values, and Types in Rust
In Rust, an expression is something that gets evaluated to produce a value of a specific type. You’ll be working with values all the time in programming, whether it’s a number like 9
, a boolean like true
, or a string like "Hello, world!"
.
Your computer stores these values in memory, and you use them to perform operations, like calling functions or using macros.
Values and Types
Every value in Rust has a type, which helps the computer understand how much space to allocate for that value in memory. Types also help you as a programmer by preventing mistakes, such as trying to add the number 5
to the string "banana"
. Rust’s type system ensures that your code is both safe and efficient.
Expressions in Rust
In Rust, we create values using expressions. An expression is essentially a set of instructions that tells the computer how to produce a value. Here are a few examples:
- The expression
3 + 4
means “add the numbers 3 and 4 together to produce a value.” - The expression
apples * 2
means “take the value stored in the variableapples
and multiply it by 2.” - The expression
5
is a literal, meaning it doesn’t need to be evaluated—it already represents the value 5.
You can also call functions using expressions, like this:
add(7, 8)
This expression tells the computer to call the add
function with the values 7 and 8.
Combining Expressions
You can create more complex expressions by combining simpler ones. For instance:
add(3 + 4, apples * 2)
In this case, instead of passing the literal value 7
to the add
function, we pass the expression 3 + 4
. Similarly, instead of using a fixed value for the second argument, we use apples * 2
. If apples
equals 4
, both versions of the function call would produce the same result.
Evaluating Expressions
The process of turning an expression into a value is called evaluation. You can think of this process like a tree, where each operation branches out to smaller, simpler expressions. For example, consider the expression:
(3 + 4) * (1 + (5 - 2))
Here’s what its evaluation tree might look like:
*
/ \
+ +
/ \ / \
3 4 1 -
/ \
5 2
Each operation is evaluated from the leaves of the tree up to the root, producing the final value.
Exercise
Try drawing an expression tree for this:
(3 + 4) * (1 + (5 - 2))
Grab a piece of paper and map out the tree, starting from the deepest parts of the expression!
Effects Versus Results in Rust
Let’s look at this expression:
println!("Hello, world!")
What is the value of this expression? The interesting thing here is that println!
doesn’t produce a value you can use. Instead, it performs a side effect—in this case, printing the message "Hello, world!"
to the console.
Expressions with Results
Most expressions in Rust evaluate to a value. For example, the expression 3 + 4
evaluates to the value 7
. You can take this result and store it in a variable, pass it to a function, or use it in another expression. These types of expressions produce results that can be used elsewhere in your program.
Expressions with Side Effects
On the other hand, some expressions, like println!
, don’t produce a value you can work with—they instead perform a side effect. A side effect is when an expression does something (like printing text, modifying a file, or sending a network request) but doesn’t return a result that you can store or manipulate.
In Rust, you’ll encounter both types of expressions: those that produce results and those that perform side effects. Let’s look at an example of both:
fn main() {
let sum = 3 + 4; // This expression produces a result
println!("The sum is: {}", sum); // This expression performs a side effect
}
Here, the expression 3 + 4
evaluates to the value 7
, which is then stored in the variable sum
. In contrast, the println!
expression prints a message to the console, but it doesn’t produce a value you can assign to a variable.
Understanding When to Expect a Value or a Side Effect
As you write Rust code, you’ll need to recognize whether an expression will return a value or cause a side effect. Rust makes it easy to distinguish between the two. If an expression returns a value, you can assign it to a variable. If it causes a side effect, you’ll often use it without storing anything, like the println!
macro.
Rust encourages clear code by making it obvious when you’re using expressions that produce values and when you’re executing side effects. Understanding this distinction will help you write more efficient and readable programs.
Exercise
Take a look at this code and identify which parts are producing values and which are performing side effects:
fn main() {
let x = 10 * 2; // Expression producing a value
println!("x is: {}", x); // Expression with a side effect
let y = x / 2; // Another expression producing a value
println!("y is: {}", y); // Another expression with a side effect
}
In this exercise, the arithmetic expressions like 10 * 2
and x / 2
produce values, while the println!
macros perform side effects by printing the values.
Blocks and Statements in Rust
In Rust, code is organized into statements and blocks. Let’s break down what that means and how they work together.
Statements
A statement is a command that performs an action but doesn’t produce a value you can use. For example, variable declarations in Rust are statements:
fn main() {
let x = 5; // This is a statement
}
Here, let x = 5;
is a statement that declares a variable x
and assigns it the value 5
. Statements end with a semicolon (;
), and they don’t return a value.
You can also have function calls as statements, like this:
fn main() {
println!("Hello, world!"); // This is also a statement
}
The println!
macro is executed, but it doesn’t return a value you can assign to a variable. Instead, it performs a side effect by printing a message to the console.
Blocks
A block in Rust is a collection of statements or expressions enclosed in curly braces {}
. Blocks can appear anywhere you want to group code together. They can also return a value, which makes them powerful tools for structuring logic.
Here’s an example of a block:
fn main() {
let y = {
let x = 5;
x + 2
}; // This block evaluates to 7 and is assigned to y
println!("The value of y is: {}", y);
}
In this code, the block inside the let y
declaration contains two statements:
let x = 5;
(a statement that declares and assigns a value tox
)x + 2
(an expression that evaluates to7
)
Notice that the block itself does not end with a semicolon. This allows the expression x + 2
to be the final value of the block, which is then assigned to y
. Blocks can act like little functions that group several steps together and return a value.
Expressions in Blocks
One of the great things about Rust is that it treats blocks as expressions that can produce a value. This means you can use blocks to calculate values, like in the example above. However, if a block ends with a semicolon, it doesn’t return a value:
fn main() {
let x = {
let a = 2;
let b = 3;
a + b; // This block does NOT return a value, because of the semicolon
};
println!("x is: {:?}", x); // x will be assigned ()
}
Here, the semicolon after a + b
means the block doesn’t return the result of the expression. Instead, it returns the unit type ()
, which means "no value." So be careful with where you place your semicolons in Rust!
Nesting Blocks
Blocks can also be nested within each other. This allows you to build complex logic in a structured way:
fn main() {
let result = {
let x = 5;
let y = {
let z = 10;
z + x // This evaluates to 15
};
y * 2 // This evaluates to 30
};
println!("The result is: {}", result);
}
In this case, we have two nested blocks. The inner block evaluates to 15
, and the outer block evaluates to 30
, which is then printed.
Summary
- Statements perform actions but don’t return values.
- Blocks are collections of statements and expressions enclosed in
{}
. Blocks can return values, making them more flexible than statements. - Pay attention to the semicolon! It determines whether a block returns a value or not.
- Nesting blocks lets you build more complex logic while keeping your code organized.
Exercise
Take a look at the following code and identify which parts are blocks and which are statements:
fn main() {
let x = 5; // Statement
let y = {
let z = x + 1; // Statement
z * 2 // Block returning a value
}; // Block assigned to y
println!("The value of y is: {}", y); // Statement with a side effect
}
Now, modify the code so the block inside let y
does not return a value. What happens when you try to print y
?
Summary
- Rust uses many special symbols like
{
and)
, often in pairs. - These pairs of symbols need to nest properly inside each other.
- Following layout rules is important for making your code easier to read.
- The
rustfmt
tool can automatically format your code for better readability. - You can add comments in Rust using
//
for single-line or/* */
for multi-line comments. - Expressions in Rust are evaluated to a value of a specific type.
- Most of Rust programming involves writing expressions that produce values.
- You can visualize expressions as trees, evaluating them from the bottom up.
- Variables are a useful way to capture and label the results of expressions.
- Some expressions produce side effects (like printing to the console) rather than a value.
- All expressions return a result, but when there’s nothing useful to return, Rust produces
()
, also known as unit. - The body of a function is a block, which can contain multiple statements and optionally end with an expression.
- Ending a block with a semicolon throws away the result of its final expression, causing it to produce
()
. - If a block ends with an expression without a semicolon, it returns the value of that expression.
What Does the !
Do?
In Rust, the !
is used to denote a macro rather than a function. For example, in println!("Hello, world!")
, println!
is a macro that performs code expansion before being compiled. Macros like println!
, vec!
, and format!
enable more flexible and powerful functionality than regular functions by generating code at compile-time.