Time for another Rust lesson. Following on from my last post, we're delving into Common Programming Concepts. As a web developer, I'm interested to see how Rust's stricter typing compares to the flexibility of JavaScript.
This lesson covers fundamental Rust concepts like variables, basic types, functions, comments and control flow. At the end of the lesson we're encouraged to tackle a few coding challenges but I'll cover those a little later.
Variables, the return of const
Encountering const
shortly after discussing let
was a little surprising. Initially, I believed that Rust exclusively used let
for variable declaration. However, const
operates under its own set of rules. Unlike let
, const
is always immutable, requires its type to be explicitly specified (rather than inferred), and is computed at compile time. Additionally, const
names typically adhere to an all-caps, underscore-separated format.
const MINUTES_IN_A_WEEK: u32 = 7 * 24 * 60; // 10080 minutes
Additionally, there is the concept of shadowing, where a variable can be redefined with a new value and potentially a new type. The old value and memory location are effectively replaced.
// Set the current letter to 'a'
let mut current_letter: char = 'a';
println!("Current letter: {}", current_letter); // prints 'a'
// Set current letter to 'b'
current_letter = 'b';
println!("Current letter: {}", current_letter); // prints 'b'
// 'Shadow' current_letter to create a new variable
let current_letter: String = String::from("Hello, World");
println!("Current letter: {}", current_letter); // prints "Hello, World"
// Attempt to set current_letter to another string
current_letter = String::from("Oh no!!!"); // Throws an error
Unfortunately, the last line of code fails to compile because when we shadowed current_letter
, we forgot to make the new variable mutable.
Data types, the stronger the better
As I alluded to earlier, Rust is a strongly typed language but also it is a statically typed
language. This means that not only must all data have a type but all data types must be declared at compile time and then adhered to. This is quite different from JavaScript, where types are implied and can be changed as often as needed.
In Rust, we declare a type by placing a colon next to the variable name followed by the type name, like so:
let variable_one: u32 = 16; // this is an unsigned 32bit integer.
let variable_two: char = 'c'; // this is a single character.
let variable_three: f64 = 3.14; // this is a 64bit floating point.
Types in Rust are divided into two main categories: scalar and compound. Scalar types represent a single value, while compound types represent collections of values. Let's explore these categories further.
Scalar types
Scalar types represent single values. Rust has four primary scalar types: integers
, floating-point numbers
, Booleans
and characters
. Let's explore each of these types in more detail.
Integers
Integers are whole numbers and are divided into signed and unsigned variants. In Rust, signed integers can represent both positive and negative numbers, while unsigned integers can only represent non-negative numbers. Knowing whether you need negative values can help optimize memory usage.
Floating-point numbers
Floating-point numbers represent all numbers, including those between any two integers. In Rust, floating-point numbers are always signed, meaning they can be positive or negative.
let num_var_one: f64 = 3.1415926; // PI is probably the most famous float
let num_var_two: f64 = 2.0; // even whole numbers must include at least one decimal place
Boolean
Boolean values represent simple true/false values, as in most other programming languages. Interestingly, the term "Boolean" is named after a British mathematician named George Boole.
Character
The char type is used for storing a single character. Unlike some expectations, the char type in Rust has a memory footprint of 32 bits, ensuring there is enough space to represent characters from various languages that may require more memory.
Compound types
Compound types are collections of multiple values expressed within a single type. These values can be of the same type or different types, but the type consistency must be determined at compile time. Rust primarily supports two compound types: tuples
and arrays
.
Tuples
A tuple is a grouping of several values with potentially different types under one let or const. You can access these values either through destructuring or by their index.
// Declare the tuple
let tuple_variable: (f64, char, i32) = (3.1415, 'c', -7);
// Prints: float 3.1415, character c, integer: -7
println!("float {}, character {}, integer: {}", tuple_variable.0, tuple_variable.1, tuple_variable.2);
// Destructure the tuple
let (float, character, integer) = tuple_variable;
// Prints: float 3.1415, character c, integer: -7
println!("float {}, character {}, integer: {}", float, character, integer);
Arrays
An array also groups data together, but unlike tuples, all values in an array must be of the same type. Additionally, arrays have a fixed length, which is determined at compile time. However, the Rust book hints at a concept called a vector
, which will be introduced in later lessons and supports variable-length arrays.
// Declare the array
let array_variable: [char; 3] = ['a', 'b', 'c'];
// Prints: a, b, c
println!("{}, {}, {}", array_variable[0], array_variable[1], array_variable[2]);
// Destructure the array
let [a, b, c] = array_variable;
// Prints: a, b, c
println!("{}, {}, {}", a, b, c);
Functions
In Rust, functions play a crucial role, with main often serving as the entry point. Rust functions start with the fn keyword and follow the convention of snake case for their names.
fn main() {
print_function();
}
fn print_function() {
println!("String to print...");
}
Functions can also accept arguments (or parameters) passed from outside the function. These arguments exist within the function's scope and must be declared with a name and type.
fn main() {
add_five_and_log(12);
}
fn add_five_and_log(num: u32) {
// prints 17
println!("{}", num + 5);
}
While many functions perform actions, sometimes you need a function to return a value. Rust provides two methods for returning values: using the return keyword or simply expressing the value without a semicolon at the end of the line. Regardless of the method chosen, the function must declare its return type using -> after the function name.
fn main() {
let add = add_five_and_log(12);
let minus = minus_five_and_log(12);
// prints 17 and 7
println!("{} and {}", add, minus)
}
fn add_five_and_log(num: u32) -> u32 {
// using the return keyword
return num + 5;
}
fn minus_five_and_log(num: u32) -> u32 {
// return without the keyword (implicit return)
num - 5
}
While both methods achieve the same result, some may find the syntax without the return keyword a bit unconventional at first.
Comments
In programming, comments play a crucial role in enhancing code readability and facilitating code sharing among developers, including your future self. Whilst well-named variables and functions contribute to clarity, code documentation remains, in my opinion, vital for comprehensibility and maintainability.
In Rust, comments are denoted by //, allowing you to add comments anywhere on a line. This flexibility enables you to clarify complex logic or provide brief explanations alongside code snippets.
// Comments can go here
let top_comment: char = '^';
let side_comment: char = '>'; // Comments can also go here
// Multi-line comments are useful for longer explanations
// or comments that span multiple lines
Interestingly, the book hints at "documentation comments," which will be explored in more detail later on.
Control Flow
In this section, we delve into the foundational concepts of control flow in Rust, encompassing if statements and loops. While these constructs may seem familiar, let's take a closer look at how they operate within the Rust programming paradigm.
If statements
An if statement in Rust allows for the execution of different code blocks based on Boolean conditions. While developers familiar with JavaScript might be accustomed to working with truthy and falsy values, Rust requires explicit Boolean evaluations.
The fundamental keywords associated with if statements are if
, else
and else if
, with the latter serving as a combination of the former two.
let number: i32 = 7;
if number < 0 { // Evaluates as false
println!("Number is negative."); // Skipped
} else if number < 10 { // Evaluates as true
println!("Number is less than 10."); // Executed
} else if number < 100 { // Not executed
println!("Number is less than 100."); // Not executed
} else { // Executed
println!("Number is 100 or greater."); // Not executed
}
This, to my JavaScript-trained brain, resembles a switch
statement. However, Rust offers the match
construct, though we won't learn about that for a few lessons yet.
Additionally, Rust allows for inline usage of if statements to set variable values, although all values returned by the if
blocks must be of the same type.
let is_leap_year = 2024 % 4 == 0;
let days_in_year = if is_leap_year { 366 } else { 365 };
loops
Loops in Rust enable the repetition of a block of code multiple times, whether it's for iterating over an array or resetting a flow back to the beginning after completion. Rust offers three primary types of loops: loop
, while
and for
. Let's explore each of them in detail.
loop
The loop
construct in Rust creates an infinite loop, which continues to execute until it is explicitly told to stop. Within a loop, two keywords play crucial roles: continue
and break
.
-
continue
: It allows you to skip the current iteration and proceed to the next one. -
break
: This keyword terminates the loop prematurely, allowing the execution of code outside the loop. Additionally, whenbreak
is used, you can return a value from the loop.
let mut counter = 0; // Initialize a mutable counter variable
let result = loop { // Start an infinite loop
counter += 1; // Increment the counter by 1
if counter == 10 { // If counter reaches 10
break counter * 2; // Exit the loop and return the value 20
}
};
// Now, 'result' holds the value 20 for further processing.
In situations where there are nested loops, it's essential to specify which loop to break or continue. This can be achieved by assigning a name to loops.
let mut total = 5; // Initialize the total variable
'outer_loop: loop {
let mut iteration = 0; // Initialize the iteration counter
loop {
iteration += 1; // Increment the iteration counter
println!("Iteration {} with {} loops remaining.", iteration, total);
if iteration == 5 { // If iteration reaches 5
if total == 0 { // Check if total is zero
break 'outer_loop; // Exit the outer loop
}
break; // Exit the inner loop
}
}
total -= 1; // Decrement the total variable
}
while
The while
loop in Rust allows you to repeatedly execute a block of code as long as a certain condition remains true. It's particularly useful when the number of iterations is not predetermined. While
loops still rely on a mutable variable outside the loop to track the loop's state.
let mut count = 0; // Initialize a mutable count variable
while count < 5 { // Continue looping as long as count is less than 5
println!("Count: {}", count); // Print the current value of count
count += 1; // Increment count by 1 in each iteration
}
for
The for
loop in Rust is primarily used for iterating over a sequence of values, such as arrays or ranges. It simplifies iteration compared to a while
loop. In the for
loop syntax, each item in the array is accessed using a named variable.
let numbers = [1, 2, 3, 4, 5]; // Define an array of numbers
for num in numbers { // Iterate over each element in the array
println!("Number: {}", num); // Print each element
}
Challenges
Now, let's put our newfound understanding of Rust to the test with a couple of challenges:
- Temperature Converter: Create a script that can convert temperatures between Fahrenheit and Celsius.
-
Fibonacci Generator: Develop a script that can generate the
nth
number in the Fibonacci sequence.
Feel free to attempt these challenges and share your solutions in the comments. Below, you'll find my solutions for reference.
My solutions
Temperature converter.
Fibonacci calculator
Another lesson done
And there we have it, another lesson completed! It's incredible to think that we're already three lessons into this journey (we covered two in part one), but the progress we've made is undeniable. While it might not feel like a significant distance covered, the knowledge gained along the way is invaluable. It seems like the pace of learning is about to ramp up!
Thanks so much for reading. If you'd like to connect with me outside of Dev here are my twitter and linkedin come say hi 😊.
Top comments (13)
Great article, thanks!
I think it could've been a good opportunity to also demonstrate using functions and returning a val from them, especially for fib which is usually done recursively.
Also it might've been a better practice to first break out of the input loop before looping to get the fib value.
A recursive function would have made sense for the Fibonacci sequence, I wanted to play with loops as that's what the lesson seemed to hunt towards but you're absolutely right.
Though remember my solutions are not the definitive answer just the way I happened to solve the problem ☺️
Nice article 😁
I think I am starting to love these Rust articles, great content here! 🔥
Thank you ☺️
Great article, Keep eyes on it
Great Article
Thank you ☺️
Ok. I think that I'm into Rust studies has a couple of months but didn't saw these "named loops" or even the break being a literally if condition.
Amazing content, dude! Hope to see more of it here.
I'm glad you're enjoying the series ☺️
This is an article I can see myself referencing next time I pick up rust! Nice job
That's the best compliment you could give, thank you ☺️
Really good article 😊, by the way do write an article about the generic types and traits, it'd be very helpful.