Learning Rust: Grasping the concepts

Andrew Bone - Mar 4 - - Dev Community

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.

Corro the Unsafe Rusturchin

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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...");
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

confused looking at paper meme

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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, when break 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.
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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 😊.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .