Rust Mastery Week #2

SOUMITRA-SAHASOUMITRA-SAHA
17 min read

Welcome back to our Rust programming journey! This is week #2. Today, we'll dive into some crucial concepts such as statements vs. expressions, comments, control flow, loops, structs, enums, options and results, pattern matching, and using modules to organize and reuse code. Let's get started!

You can find the Previous Blogs in this series here:

Statement vs. Expression

In Rust, understanding the difference between statements and expressions is fundamental. This concept might seem a bit abstract at first, but it's essential for mastering the language.

Statements

Statements are instructions that perform some action but do not return a value. They are like commands you give to the computer.

fn main() {
    let x = 5; // This is a statement
}

Expressions

Expressions, on the other hand, are evaluated to a value. Almost everything in Rust is an expression, including function calls and blocks of code.

fn main() {
    let y = {
        let x = 3;
        x + 1 // This block is an expression that evaluates to 4
    };
    println!("The value of y is: {}", y); // Outputs: The value of y is: 4
}

In JavaScript, you can compare this to the distinction between statements and expressions as well.

let x = 5; // Statement
let y = (function() { let x = 3; return x + 1; })(); // Expression
console.log(y); // Outputs: 4

Comments

Comments are an essential part of any codebase, providing clarity and understanding to whoever reads your code.

Single-line Comments

Single-line comments start with //.

fn main() {
    // This is a single-line comment
    let x = 5;
}

Multi-line Comments

Multi-line comments are enclosed between /* and */.

fn main() {
    /* This is a
    multi-line comment */
    let y = 10;
}

Control Flow

Control flow statements allow you to dictate the flow of your program. Rust provides several ways to handle control flow, including if statements, else statements, and match expressions.

If Statements

fn main() {
    let number = 5;

    if number < 10 {
        println!("The number is less than 10");
    } else {
        println!("The number is 10 or greater");
    }
}

In JavaScript, it looks quite similar:

let number = 5;

if (number < 10) {
    console.log("The number is less than 10");
} else {
    console.log("The number is 10 or greater");
}

Handling Multiple Conditions with else if

When you need to handle multiple conditions in your code, Rust allows you to chain else if statements to create a sequence of checks. This is similar to what you might be familiar with in other programming languages like JavaScript or Go.

Example of else if

Here's an example demonstrating how to use else if to handle multiple conditions:

fn main() {
    let number = 7;
    if number % 4 == 0 {         
        println!("The number is divisible by 4");     
    } else if number % 3 == 0 {
        println!("The number is divisible by 3");
    } else if number % 2 == 0 {         
        println!("The number is divisible by 2");     
    } else {         
        println!("The number is not divisible by 4, 3, or 2");     
    } 
}

In this example:

  • We check if the number is divisible by 4.

  • If it's not, we check if it's divisible by 3.

  • If it's still not, we check if it's divisible by 2.

  • If none of these conditions are true, we print a default message.

Using if in a let Statement

Rust allows you to use if expressions directly in let statements to assign values based on conditions. This can make your code more concise and expressive.

Example of if in a let Statement

Here's an example demonstrating how to use if in a let statement:

fn main() {     
    let condition = true;     
    let number = if condition { 5 } else { 6 };      
    println!("The value of number is: {}", number); 
}

In this example:

Comparing with Go

  • We define a boolean variable condition.

  • We use an if expression in a let statement to assign 5 to number if condition is true; otherwise, 6.

Comparing with JavaScript

In JavaScript, you can achieve similar functionality using the ternary operator:

let condition = true; 
let number = condition ? 5 : 6;  

console.log("The value of number is:", number);

Match Statements

Rust's match statement is a powerful control flow operator that allows you to compare a value against a series of patterns.

fn main() {
    let number = 6;

    match number {
        1 => println!("One"),
        2 => println!("Two"),
        3 => println!("Three"),
        _ => println!("Not one, two, or three"), // _ is the default case
    }
}

In Go, the equivalent is the switch statement:

package main

import "fmt"

func main() {
    number := 6

    switch number {
    case 1:
        fmt.Println("One")
    case 2:
        fmt.Println("Two")
    case 3:
        fmt.Println("Three")
    default:
        fmt.Println("Not one, two, or three")
    }
}

Loops

Rust provides several looping constructs, including loop, while, and for.

Loop

A loop runs indefinitely (infinite loop) until you explicitly break out of it.

fn main() {
    let mut count = 0;

    loop {
        count += 1;
        if count == 3 {
            break;
        }
        println!("Count is {}", count);
    }
}

While Loop

A while loop runs as long as a condition is true.

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    println!("LIFTOFF!!!");
}

For Loop

A for loop allows you to iterate over a range or collection.

fn main() {
    for number in 1..4 {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

In JavaScript, a similar loop would be:

for (let number = 1; number < 4; number++) {
    console.log(number + "!");
}
console.log("LIFTOFF!!!");

Structs

Structs in Rust are a powerful way to group related data together. They allow you to define and instantiate custom types that have meaningful names and fields. Let's explore how to use structs effectively.

Defining and Instantiating Structs

Basic Struct Definition

You define a struct using the struct keyword, followed by the struct's name and its fields.

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

Instantiating a Struct

To create an instance of the User struct, you provide values for each field.

fn main() {
    let user1 = User {
        username: String::from("soumitra_saha"),
        email: String::from("soumitra@example.com"),
        sign_in_count: 1,
        active: true,
    };
}

Using the Field Init Shorthand

When variables and fields have the same name, you can use a shorthand for field initialization.

fn main() {
    let username = String::from("soumitra_saha");
    let email = String::from("soumitra@example.com");

    let user1 = User {
        username,
        email,
        sign_in_count: 1,
        active: true,
    };
}
Creating Instances from Other Instances with Struct Update Syntax

You can create new instances by copying fields from an existing instance.

fn main() {
    let user1 = User {
        username: String::from("soumitra_saha"),
        email: String::from("soumitra@example.com"),
        sign_in_count: 1,
        active: true,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

Using Tuple Structs Without Named Fields

Tuple structs allow you to create types without named fields, useful for simple groupings.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);

    println!("Color: ({}, {}, {})", black.0, black.1, black.2);
    println!("Point: ({}, {}, {})", origin.0, origin.1, origin.2);
}

Unit-Like Structs Without Any Fields

You can also define structs that don't have any fields! These are called unit-like structs because they behave similarly to (), the unit type. Unit-like structs are useful when you need to implement a trait but don't need any data.

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Adding Useful Functions with Derived Traits

We can add useful functions to our structs using derived traits like Debug.

#[derive(Debug)]
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        username: String::from("soumitra_saha"),
        email: String::from("soumitra@example.com"),
        sign_in_count: 1,
        active: true,
    };

    println!("{:?}", user1);
}

Method Syntax

Rust allows you to define methods on structs to add functionality.

Defining Methods

Methods are similar to functions, Methods are different from functions in that they’re defined within the context of a struct.

Methods are defined within an impl block.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("The area of the rectangle is {} square pixels.", rect1.area());
}

Methods with More Parameters

You can define methods that take additional parameters.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };

    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Associated Functions

Associated functions are functions that are not methods because they do not operate on an instance of the struct. They are often used as constructors.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

fn main() {
    let sq = Rectangle::square(30);

    println!("The square has width {} and height {}.", sq.width, sq.height);
}

Multiple impl Blocks

You can split the implementation of a struct into multiple impl blocks.

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };

    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("The area of rect1 is {} square pixels.", rect1.area());
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Option/Result

Rust does not have null values. Instead, it has the Option and Result types to handle absence and potential errors.

Option

The Option type is used when a value can be either something or nothing.

fn main() {
    let some_number = Some(5);
    let no_number: Option<i32> = None;

    println!("some_number is: {:?}", some_number);
    println!("no_number is: {:?}", no_number);
}

Result

The Result type is used for functions that can return a value or an error.

use std::fs::File;

fn main() {
    let file = File::open("hello.txt");

    match file {
        Ok(file) => println!("File opened successfully: {:?}", file),
        Err(error) => println!("Problem opening the file: {:?}", error),
    }
}

Enums in Rust

Enums are a powerful feature in Rust that allows you to define a type by enumerating its possible values. Enums can hold different kinds of data and provide a way to express a value that can take on multiple forms.

Defining an Enum

Basic Enum Definition

To define an enum, you use the enum keyword, followed by the name of the enum and its variants.

enum Direction {
    North,
    East,
    South,
    West,
}

Each variant of the enum can be a different type or hold different amounts of data.

Enum Values

You can associate data with each variant. For instance, here's an enum that represents different kinds of messages with associated data:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

The Option Enum and Its Advantages over Null Values

Rust doesn't have null values, which helps avoid null pointer exceptions. Instead, Rust has an Option enum, which is used to represent an optional value:

enum Option<T> {
    Some(T),
    None,
}

Here's how you might use Option:

fn main() {
    let some_number = Some(5);
    let some_string = Some("A string");

    let absent_number: Option<i32> = None;
}

Using Option forces you to handle the cases where a value might be absent, making your code safer.

The match Control Flow Operator

The match statement in Rust allows you to compare a value against a series of patterns and execute code based on which pattern matches.

Patterns That Bind to Values

Patterns can bind to the values they match, allowing you to use those values in your code.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
    let msg = Message::Move { x: 10, y: 20 };

    match msg {
        Message::Move { x, y } => {
            println!("Move to x: {}, y: {}", x, y);
        }
        _ => {}
    }
}

Matching with Option<T>

You can use match with the Option enum to handle optional values:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
        None => None,
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

    println!("six: {:?}", six);
    println!("none: {:?}", none);
}

Matches Are Exhaustive

Every possible value of the enum must be covered in a match statement. This ensures you handle all cases, making your code more robust.

fn main() {
    let direction = Direction::South;

    match direction {
        Direction::North => println!("Going North!"),
        Direction::East => println!("Going East!"),
        Direction::South => println!("Going South!"),
        Direction::West => println!("Going West!"),
    }
}

The _ Placeholder

The _ placeholder can be used to match all remaining cases:

fn main() {
    let number = 7;

    match number {
        1 => println!("One!"),
        2 => println!("Two!"),
        _ => println!("Something else!"),
    }
}

Concise Control Flow with if let

The if let syntax provides a concise way to handle enums when you only care about one variant.

fn main() {
    let some_value = Some(3);

    if let Some(3) = some_value {
        println!("The value is three!");
    }
}

You can also handle a default case using else:

fn main() {
    let some_value = Some(3);

    if let Some(3) = some_value {
        println!("The value is three!");
    } else {
        println!("The value is not three.");
    }
}

Enums in Rust provide a flexible way to define types that can take on multiple forms, making your code more expressive and robust. The combination of enums with match statements and if let allows you to handle different cases cleanly and safely.

Using Modules to Reuse and Organize Code

Modules let you organize your code into namespaces, making it easier to manage large codebases.

Defining a Module

mod my_module {
    pub fn say_hello() {
        println!("Hello!");
    }
}

Using a Module

fn main() {
    my_module::say_hello();
}

Module File Structure

Rust modules can be organized into separate files for better readability and maintainability.

src/
|-- main.rs
|-- my_module.rs

In main.rs:

mod my_module;

fn main() {
    my_module::say_hello();
}

In my_module.rs:

pub fn say_hello() {
    println!("Hello!");
}

Most Common Doubts

  1. Can I Use a Different impl Block Name for a Struct in Rust?

    • Question: Can I implement methods for a struct using a different impl block name in Rust?

    • Answer: No, you cannot. The impl block name must match the struct it is implemented. If you try to use a different name, Rust won't recognize the association and will throw a compilation error. Always use the exact struct name for the impl block to ensure Rust correctly links the methods to the struct.

  2. What is the Difference Between Statements and Expressions in Rust?

    • Question: How do statements and expressions differ in Rust?

    • Answer: In Rust, expressions evaluate to a value, while statements do not. For example, let x = 5; is a statement, but x + 1 is an expression. Expressions can be part of statements, but not the other way around.

  3. How Do I Write Comments in Rust?

    • Question: What are the types of comments in Rust?

    • Answer: Rust supports two types of comments: line comments and block comments. Line comments start with //, while block comments are enclosed within /* */. Use comments to explain your code and make it more readable.

  4. What Control Flow Constructs Are Available in Rust?

    • Question: What control flow structures can I use in Rust?

    • Answer: Rust provides several control flow constructs, including if, else if, else, loop, while, and for. These constructs help manage the flow of execution in your programs.

  5. How Do I Use Loops in Rust?

    • Question: What types of loops can I use in Rust?

    • Answer: Rust offers three types of loops: loop, while, and for. The loop construct allows infinite looping until you explicitly break, while while and for provide more controlled iteration.

  6. How Do I Define and Instantiate Structs in Rust?

    • Question: How do I create and use structs in Rust?

    • Answer: You define a struct using the struct keyword, followed by its name and fields. Instantiate a struct by specifying values for its fields. For example:

        struct Person {
            name: String,
            age: u8,
        }
      
        let person = Person {
            name: String::from("Alice"),
            age: 30,
        };
      
  7. What is the Field Init Shorthand in Rust?

    • Question: How can I use the field init shorthand when variables and fields have the same name?

    • Answer: When a variable and a struct field have the same name, you can use the field init shorthand to simplify initialization:

        let name = String::from("Alice");
        let age = 30;
      
        let person = Person { name, age };
      
  8. How Do I Create Instances from Other Instances with Struct Update Syntax?

    • Question: What is the struct update syntax in Rust?

    • Answer: The struct update syntax allows you to create a new instance of a struct using values from an existing instance:

        let new_person = Person {
            age: 25,
            ..person
        };
      
  9. What are Tuple Structs and Unit-Like Structs?

    • Question: What are tuple structs and unit-like structs in Rust?

    • Answer: Tuple structs are structs without named fields, useful for creating different types:

        struct Color(u8, u8, u8);
        let red = Color(255, 0, 0);
      

      Unit-like structs have no fields and are used for specific purposes:

        struct Unit;
      
  10. How Do I Define Methods for Structs in Rust?

    • Question: How can I add methods to structs in Rust?

    • Answer: You define methods within an impl block for the struct:

        impl Person {
            fn greet(&self) {
                println!("Hello, my name is {}.", self.name);
            }
        }
      
        person.greet();
      
  11. How Do Enums Work in Rust?

    • Question: What are enums and how do I use them in Rust?

    • Answer: Enums in Rust allow you to define a type with multiple possible variants. Each variant can hold different data:

        enum Direction {
            North,
            East,
            South,
            West,
        }
      
  12. What is the Option Enum and Why is it Useful?

    • Question: How does the Option enum work and what are its advantages?

    • Answer: The Option enum represents an optional value, replacing nulls and preventing null pointer errors:

        enum Option<T> {
            Some(T),
            None,
        }
      
  13. How Do I Use the match Operator in Rust?

    • Question: How can I use the match control flow operator in Rust?

    • Answer: The match operator allows you to compare a value against multiple patterns and execute code based on which pattern matches:

        match direction {
            Direction::North => println!("Going North!"),
            Direction::East => println!("Going East!"),
            _ => println!("Going somewhere else!"),
        }
      
  14. What is the _ Placeholder in Rust?

    • Question: What does the _ placeholder do in Rust?

    • Answer: The _ placeholder in match statements act as a catch-all for any unmatched cases, ensuring your match is exhaustive:

        match number {
            1 => println!("One!"),
            _ => println!("Something else!"),
        }
      
  15. How Do I Use if let for Concise Control Flow?

    • Question: What is if let and how do I use it in Rust?

    • Answer: if let is a concise way to handle a single pattern match, especially useful for matching Option values:

        if let Some(3) = some_value {
            println!("The value is three!");
        }
      
  16. What is the Result enum in Rust?

    • Question: How does the Result enum work in Rust?

    • Answer: The Result enum is used for error handling, representing either success (Ok) or failure (Err):

        enum Result<T, E> {
            Ok(T),
            Err(E),
        }
      
  17. How Does Pattern Matching Work in Rust?

    • Question: What is pattern matching and how do I use it in Rust?

    • Answer: Pattern matching allows you to destructure and match complex data structures in a readable way, commonly used with enums and structs.

  18. How Do I Handle Mutability in Rust?

    • Question: What are the rules for mutability in Rust?

    • Answer: In Rust, variables are immutable by default. Use the mut keyword to make a variable mutable:

        let mut x = 5;
        x = 6;
      
  19. How Do I Use Modules to Organize Code in Rust?

    • Question: How can I use modules to reuse and organize code in Rust?

    • Answer: Modules help organize code by grouping related functions, structs, and enums. Use the mod keyword to define a module:

        mod my_module {
            pub fn my_function() {
                println!("Hello from my_module!");
            }
        }
      
        my_module::my_function();
      
0
Subscribe to my newsletter

Read articles from SOUMITRA-SAHA directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

SOUMITRA-SAHA
SOUMITRA-SAHA

About Me I am a Full-Stack Developer with approximately two years of experience in the industry. My expertise spans both front-end and back-end development, where I have worked extensively to build and maintain dynamic, responsive web applications. Technical Skills Front-End Development: HTML, CSS, JavaScript, Typescript Frameworks: React, NextJs, Remix, Vite Libraries: TailwindCSS, Bootstrap Tools: Webpack, Babel Back-End Development: Languages: Typescript, Node.js, Python, Go, Rust Frameworks: NextJS, Remix, Express.js, Django, Go (Gin), Rust Databases: PostgreSQL, MySQL, MongoDB Tools: Docker, Kubernetes DevOps: Version Control: Git, GitHub CI/CD: Jenkins, CircleCI Infrastructure as Code: Terraform Cloud Services: AWS, Lamda Other Skills: RESTful API Development Test-Driven Development (TDD), (vitest, jest, Cypress) Agile Methodologies Personal Interests I am passionate about learning new technologies and keeping up with industry trends. I enjoy exploring innovative solutions to complex problems and continuously seek opportunities to expand my expertise. In my free time, I engage with the developer community, contributing to open-source projects and participating in tech meetups.