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
- Using Paths
- The
pub
keyword - Structs and Enums inside a module
- The
super
andself
keywords - Scope management with the
use
keyword - Using external packages
- Nested paths and globing
- Separate your modules in different files
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 inscr/main.rs
) will be named "building" too. If we wrote a library crate (insrc/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
}
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 packagehotel
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")
}
}
}
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
}
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
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
}
}
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
}
}
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(),
}
}
}
}
And add the following in src/main.rs
:
use hotel::guests;
fn main() {
hotel::go_on_vacation();
}
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 number10
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
);
}
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
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);
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 }
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,
}
}
}
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!");
}
}
And add the following to the main function in src/main.rs
:
fn main() {
// snip
hotel::guests::eat_meal();
}
If we try to run it, we will get the following result as expected
Eating Diner
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();
}
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!");
}
}
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"
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;
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};
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;
We simply use the self
keyword which we talked about earlier:
use hotel::facilities::restaurants::{self, prepare_tables};
And we can use "globing" using "*" to bring "everything" (that is public) into scope from a module:
use hotel::facilities::restaurants::*
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:
- Write the module contents in a separate file inside
src
directory. - Use
mod <module name>
in its parent module (or binary crate) where is the same as its file name. - 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(),
}
}
}
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() {}
// 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
}
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:?}");
}
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 ๐
Top comments (0)