Learning Rust šŸ¦€: 15 - How you can organize your Rust code with "Modules"

fadygrab

Fady GA šŸ˜Ž

Posted on October 1, 2023

Learning Rust šŸ¦€: 15 - How you can organize your Rust code with "Modules"

In this article we will see how the Rust's Module system works and how we can group related code together to enhance our project's maintainability. Let's jump in!

āš ļø Remember!

You can find all the code snippets for this series in its accompanying repo

If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page. That said, the playground may not be suitable for this topic.

āš ļøāš ļø The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

ā­ I try to publish a new article every week (maybe more if the Rust gods šŸ™Œ are generous šŸ˜) so stay tuned šŸ˜‰. I'll be posting "new articles updates" on my LinkedIn and Twitter.

Table of Contents:

Defining Modules:

What is a module in Rust? A module in Rust is just a grouping of related code together. We can define all of our modules in a single file or separate them into different files, which proves very handy if our project code base is large.

But before I show you how to define a module in Rust, I just want to demonstrate how a Rust project is structured using cargo. When we type cargo new <package name> into our terminal, cargo makes a "package" structure. A package contains "crates" which are in the src directory. Crates can be either a library crate or a binary crate. The difference between the two is that a library crate doesnt contain a main function and it just provides some functionality to the binary crate(s) in the same package or when used as an external crate.

A package can contain only one library crate and one or more binary crates.

So far, all the packages we have created contain only one binary crate.

In the src directory, the binary crate root is the main.rs and the library crate is the lib.rs. The root crate will have the same name as the package's. If our package contains binary crates other than the one in main.rs, we put them in src/bin/ directory and they will be compiled into executables alongside our default binary crate.

If we typed cargo new building, it will create a "building" package and the root binary crate (written in scr/main.rs) will be named "building" too. If we wrote a library crate (in src/lib.rs), it will also have the same name - building - as the package.

Now that we've got this introduction out of our way, let's see how we can define a module:

mod my_module {
    // module contents
}
Enter fullscreen mode Exit fullscreen mode

That's it! šŸ™‚

To better demonstrate Rust's module system, let's assume that we are building a digital "hotel" in Rust and we will use the modules system to organize similar code together.

Type cargo new hotel and create a lib.rs in the src directory.

We can type cargo new hotel --lib and this will create a package hotel with only one library crate.

Next, put the following module in our recently created lib.rs file.

mod reception {
    mod booking {
        fn book_room() {
            println!("Booking room!")
        }
        fn cancel_booking() {}
    }

    mod guests_management {
        fn guest_checkin() {
            println!("Checking in!")
        }
        fn guest_checkout() {}
        fn receive_guest_request() {}
    }
}

mod facilities {
    mod house_keeping {
        fn clean_room() {}
        fn deliver_meal() {}
    }

    mod maintenance {
        fn pool_maintenance() {}
        fn electrical_maintenance() {}
        fn building_maintenance() {}
    }

    mod restaurants {
        fn prepare_tables() {
            println!("Preparing table")
        }
        fn make_meal() {
            println!("Making meal")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

A module in Rust can contain other items such as functions, structs, enums, constants, traits, and other modules. Here we have two main modules, reception and facilities both containing "child" modules that contain function.

Using Paths:

To access an item inside a module we use a "file system-like" path which can be either absolute or relative. To demonstrate that, write the following function in src/lib.rs outside of any module.

pub fn go_on_vacation() {
    crate::reception::booking::book_room();         // Absolute path
    reception::guests_management::guest_checkin();  // Relative path
}
Enter fullscreen mode Exit fullscreen mode

This function calls two other function from the reception module. It will be a lot easier to understand the module paths if we imagined our lib.rs as the following file system:

crate
    ā”œā”€ā”€ reception
    |       ā”œā”€ā”€ booking
    |       |       ā”œā”€ā”€ book_room
    |       |       ā””ā”€ā”€ cancel_booking
    |       ā””ā”€ā”€ guests_management
    |               ā”œā”€ā”€ guest_checkin
    |               ā””ā”€ā”€ guest_checkout
    ā””ā”€ā”€ facilities
            ā”œā”€ā”€ house_keeping
            |       ā”œā”€ā”€ clean_room
            |       ā””ā”€ā”€ deliver_meal
            ā”œā”€ā”€ maintenance
            |       ā”œā”€ā”€ pool_maintenance
            |       ā”œā”€ā”€ electrical_maintenance
            |       ā””ā”€ā”€ building_maintenance
            ā””ā”€ā”€ restaurants
                    ā”œā”€ā”€ prepare_tables
                    ā””ā”€ā”€ make_meal
Enter fullscreen mode Exit fullscreen mode

Where the crate keyword represents the root crate or the "/" (root) in a Linux file system.

In the first call, we used an absolute path where we started with the root crate (crate keyword) then all the way to the book_room function. In the second call, we use a relative path as both the go_on_vacation function and the reception module are on the same level (children of the root crate), we can refer to the reception module from the go_on_vacation function like we did.

Unfortunately, if we tried to run this code, it wouldn't compile! The compilier will complain about booking and guests_management modules being private!

The pub keyword:

In order to solve the problem we saw in the previous section, we can use the pub (for public) keyword. Using pub before mod will make the module public. Armed with our new found knowledge, let's add the pub keyword as follows:

mod reception {
    pub mod booking {
        // snip
    }

    pub mod guests_management {
        // snip
    }
}
Enter fullscreen mode Exit fullscreen mode

If we run the code now, the compiler will complain, again!

This time, it will complain about book_rook and guest_checkin functions being private. This highlights an important rule about the pub keyword, making a module public won't make its children public! and you will have to explicitly choose which components for public access as they are private by default. Let's do the same for those functions as we did with the modules:

mod reception {
    pub mod booking {
        pub fn book_room() {
            println!("Booking room!")
        }
        // snip
    }

    pub mod guests_management {
        pub fn guest_checkin() {
            println!("Checking in!")
        }
        // snip
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, it will be compiled! But wait! we didn't put pub before reception module, how we could access it?

As the reception module and the go_on_vacation function are both on the same level in the "modules system" (the root crate), they can see each other.

Items that are on the same level are called "siblings" while items that are one level down are called "children" and the items that are one level up are called "parents".

Structs and Enums inside a module:

As I've mentioned before, modules can contain Structs and Enums and both of them (like functions) can accept the pub keyword but with a subtle difference! First, let's add a Room struct to the reception module.

mod reception {
    // snip
    pub mod guests_management {
        // snip
        pub fn get_room_number() -> i8 {
            10
        }
    }
    #[derive(Debug)]
    pub struct Room {
        pub view: String,
        pub beds_count: i8,
        number: i8,
    }
    impl Room {
        pub fn new(view: &str, beds: i8) -> Room {
            Room {
                view: String::from(view),
                beds_count: beds,
                number: guests_management::get_room_number(),
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And add the following in src/main.rs:

use hotel::guests;
fn main() {
    hotel::go_on_vacation();
}
Enter fullscreen mode Exit fullscreen mode

Okay! There is a lot going on here, so let's break it down! šŸ˜‰
Let's start first with the Struct definition. One thing to notice is that both the struct and some of its elements have the pub keyword. For structs if you are going to make the public, in addition to the pub before the struct definition, you have to explicitly set which elements are public as the default behavior for them is to be private. Here we have created a Room struct that has two public elements, view and beds_count, and one private element, number. But this presents a problem! As the number property is private, how can it be set??

We solve that by creating an implementation for this struct including a new function that acts as a factory function for new Rooms. This function sets the view and beds_number according to the guest's request and sets the number using a public function, get_room_number, inside the sibling guests_management module.

The implementation of the get_room_number function is really silly! It just returns the number 10 for every request! A better function would check the rooms in a database for example and return a vacant one. But this one here will do the trick.

Add the following code to the go_on_vacation function and run it:

pub fn go_on_vacation() {
    // snip

    let my_room = reception::Room::new("Sea view", 2);

    println!(
        "My room's vew is {} and my room number is {}. My room: {:?}",
        my_room.view, my_room.number, my_room
    );
}
Enter fullscreen mode Exit fullscreen mode

As expected, it will produce a compilation error as follows:

error[E0616]: field `number` of struct `Room` is private
   --> src/lib.rs:100:31
    |
100 |         my_room.view, my_room.number, my_room
    |                               ^^^^^^ private field
Enter fullscreen mode Exit fullscreen mode

We are not allowed to access "private" fields of a struct even if the struct itself is "public". If we ommited the erroneous code and replaced it with:

    println!("My room's vew is {}. My room: {:?}", my_room.view, my_room);
Enter fullscreen mode Exit fullscreen mode

It will be compiled and we will be able to see that the number field of the Room struct is set to 10

My room's vew is Sea view. My room: Room { view: "Sea view", beds_count: 2, number: 10 }
Enter fullscreen mode Exit fullscreen mode

For Enums, however, if we set it to public, all of its variants will be public too without the need to explicitly set their access to public which makes sense as all the variants of an enum are checked exhaustively so we can't choose a subset to make them public. Add the following enumb to the restaurant module inside the facilities module:

mod facilities {
    // snip
    pub mod restaurants {
       // snip
        #[derive(Debug)]
        pub enum Meal {
            Breakfast,
            Brunch,
            Lunch,
            Diner,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

To test it, let's add the following new module to src/lib.rs:

pub mod guests {
    fn book_a_room() {}
    fn go_to_beach() {}
    fn go_to_pool() {}
    pub fn eat_meal() {
        let my_meal = crate::facilities::restaurants::Meal::Diner;
        println!("Eating {my_meal:?}");
    }
    fn end_vacation() {
        println!("Bye bye!");
    }
}
Enter fullscreen mode Exit fullscreen mode

And add the following to the main function in src/main.rs:

fn main() {
    // snip
    hotel::guests::eat_meal();
}
Enter fullscreen mode Exit fullscreen mode

If we try to run it, we will get the following result as expected

Eating Diner
Enter fullscreen mode Exit fullscreen mode

The super and self keywords:

super and self are very useful keywords when used in Rust's module system paths. super refers to the direct parent of the module that is calling it and self refers to the same module that is calling it.

The use of self will become evidence in the "nested paths" section.

To demonstrate their use, add this code to the book_a_room function in the guest module:

    fn book_a_room() {
        super::reception::booking::book_room();
        self::go_to_beach();
    }
Enter fullscreen mode Exit fullscreen mode

Here, the super expression is referring to the root crate as its the guest module's direct parent. Then it accesses the reception which is accessible from the root level all the way down to the book_room function.

In the expression with the self, it calls the go_to_beach function from the same guest module.

Scope management with the use keyword:

As we saw, we can use the module system paths to call functions and use elements inside modules provided that they are public. But sometimes the path can be very long! One way to overcome this is by using the use keyword as it brings to scope the element at its end and we can use it directly afterwards. Let's try it by making the following changes to the guests module:

pub mod guests {
    use super::reception::guests_management::guest_checkout;
    use crate::facilities::restaurants;

   // snip

    pub fn eat_meal() {
        restaurants::prepare_tables();
        restaurants::make_meal();
        // snip
    }
    fn end_vacation() {
        guest_checkout();
        println!("Bye bye!");
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we are bringing both the restaurants module and the guest_checkout function to scope using the use keyword. Notice that in the eat_meal function, we are writing restaurants directly without its parents as we already brought it into scope. And in the end_vacation function, we are also using the guest_checkout directly as -similar to the restaurants module- we brought it into scope.

There is a convention to use the path of the parent module if we are bringing in functions. That way, our code will be more readable as we know the source module of the function used. On the other hand, if we are bringing in structs or enums, we should use the full path.

Using external packages:

Up until this moment, we've used our own crates. To use external crates, we use the cargo.toml file in our project's root like we did here in our guessing game. We have to specify the crate name and version under the dependencies in the toml file:

rand = "0.8.5"
Enter fullscreen mode Exit fullscreen mode

Then we can use the use keyword of the path to use the functionality provided by this crate.

Nested paths and globing:

Nested paths is a way to reduce the clutter when importing several elements from the same module. Consider this example when we try to import the make_meal and prepare_tables functions from the restaurants module:

use hotel::facilities::restaurants::make_meal;
use hotel::facilities::restaurants::prepare_tables;
Enter fullscreen mode Exit fullscreen mode

The parents for both functions are the same, so we can use nested paths to reduce the preceding into this:

use hotel::facilities::restaurants::{make_meal, prepare_tables};
Enter fullscreen mode Exit fullscreen mode

And what about the following situation where we want to import the module and one of its elements?:

use hotel::facilities::restaurants;
use hotel::facilities::restaurants::prepare_tables;
Enter fullscreen mode Exit fullscreen mode

We simply use the self keyword which we talked about earlier:

use hotel::facilities::restaurants::{self, prepare_tables}; 
Enter fullscreen mode Exit fullscreen mode

And we can use "globing" using "*" to bring "everything" (that is public) into scope from a module:

use hotel::facilities::restaurants::*
Enter fullscreen mode Exit fullscreen mode

This will bring the prepare_tables and make_meal functions alongside with the Meal enum into scope.

Separate your modules in different files:

Up to this moment, our src/lib.rs contains all of our modules which is acceptable with a small code base. But what happens when our project grows bigger? One file containing all of our modules sounds like a bad idea and will decrease the code maintainability!

Luckily, we can split our modules into separate files. The rules are simple:

  1. Write the module contents in a separate file inside src directory.
  2. Use mod <module name> in its parent module (or binary crate) where is the same as its file name.
  3. If the module contains other submodules, create a directory at the same module file level and follow point 1 and 2.

Let's demonstrate that with an example. Create a new package typing cargo new hotel_different_files. Next, create src/lib.rs and add pub mod reception; to it. After that, create a new file scr/reception.rs and the following to it:

pub mod booking;
pub mod guests_management;

#[derive(Debug)]
pub struct Room {
    view: String,
    beds_count: i8,
    number: i8,
}
impl Room {
    pub fn get_room(view: &str, beds: i8) -> Room {
        Room {
            view: String::from(view),
            beds_count: beds,
            number: guests_management::get_room_number(),
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, we have referred to the booking and guests_management submodules and kept the Room struct and its implementation the same as before.

As the reception module contains submodules, we will create the src/reception/ directory which contains both submodules as src/reception/booking.rs and src/reception/guests_management.rs which will contain the following respectively:

// booking.rs
pub fn book_room() {
    println!("Booking room!")
}
pub fn cancel_booking() {}
Enter fullscreen mode Exit fullscreen mode
// guests_management.rs
pub fn guest_checkin() {
    println!("Checking in!")
}
pub fn guest_checkout() {}
fn receive_guest_request() {}

pub fn get_room_number() -> i8 {
    10
}
Enter fullscreen mode Exit fullscreen mode

And finally, to test everything out, add the following to src/main.rs:

use hotel_different_files::reception;

fn main() {
    let my_room = reception::Room::get_room("pool view", 1);
    println!("My room is {my_room:?}");
}
Enter fullscreen mode Exit fullscreen mode

And it will print the Room struct exactly as our "one-file" implementation!

Few! šŸ˜®ā€šŸ’Ø, this was a long but interesting one! Buy now, we should have a basic understanding of Rust's Module system. In the next article, We will start exploring Rust's common collections! See you then šŸ‘‹

šŸ’– šŸ’Ŗ šŸ™… šŸš©
fadygrab
Fady GA šŸ˜Ž

Posted on October 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related