A Sneak Peek into Rust Module System

Rust’s module system and the terminologies are unique to other programming languages. Understanding Rust’s module system takes a lot of reading and sometimes we are in a hurry to use the features right away. In this post, I will give you real-world examples of Rust’s module system. However, I still recommend reading the official Rust book whenever you are free.
Theory
Understanding Rust’s module system requires the knowledge of crate
, modules
and path
.
crate
: A tree of modulesmodule
: A container for better readability and organization of our code. It can contain functions, structs, traits, impl blocks and other modules too.path
: Location of a struct, function or module in the crate.
Example
Let’s take the following example of a university management program.
university_management/
└── src/
├── main.rs
├── course.rs
└── students/
└── enrollment.rs
In this program, you might want to perform the following things,
- Consuming child modules (e.g. importing
course.rs
frommain.rs
) - Consuming sibling modules (e.g. importing
course.rs
fromstudents/mod.rs
) - Consuming elements from or into nested modules. (e.g. importing
students/enrollment.rs
fromcourse.rs
or importingcourse.rs
fromstudents/enrollment.rs
)
Note: I have not defined the
students/mod.rs
file yet!
Defining Module Tree
Visually we can see the folder structure and the files but from the Rust compiler’s perspective there is only a single root module crate
. We need to declare the module tree by ourselves using the keyword mod
. And to make the students
folder a module we need a file called mod.rs
. When looking for a specific module, Rust compiler looks for a Rust file with the same name or a mod.rs
file inside a folder with the same name.
Let’s start by creating the mod.rs
file inside the students
folder.
university_management/
└── src/
├── main.rs
├── course.rs
└── students/
├── mod.rs
└── enrollment.rs
Now we actually have to declare the module tree inside the main.rs
. We can do so by adding the following code.
// main.rs
mod course;
mod students;
fn main() {
// rest of the code ...
}
The only file left is the enrollment.rs
. From the visual folder structure we can see that enrollment.rs
is nested inside the students
module. So we have to declare this nested module inside the student
module. Also in Rust everything is private by default, so to make our nested module public to the whole crate
, we need to use the pub
keyword. Add the following code in students/mod.rs
.
// students/mod.rs
pub mod enrollment;
// rest of the code ...
Now, the module tree looks like what we expected it to be!
crate university_management
├── mod course: pub(crate)
└── mod students: pub(crate)
└── mod enrollment: pub
Consuming Child Modules
The most straightforward scenario involves consuming a child module. In our case it can be importing course.rs
from the main.rs
or importing students/mod.rs
from the main.rs
or importing students/enrollment.rs
from the students/mod.rs
. We can determine the parent-child relationships of modules by where the mod
keyword is used to declare a module.
Let’s move ahead with the first scenario of importing course.rs
from the main.rs
.
Assuming we have the following code in course.rs
.
// course.rs
pub struct Course {
// rest of the code ...
}
pub fn get_course_details() {
// rest of the code ...
}
pub fn add_new_course() {
// rest of the code ...
}
As our module tree is already built we can import stuff from course.rs
using the ::
path separator.
// main.rs
mod course;
mod students;
fn main() {
let course_struct = course::Course{};
course::add_new_course();
course::get_course_details();
}
Consuming Sibling Modules
Beyond child modules, we also encounter sibling modules. Modules sharing the same parent module, such as course.rs
and students/mod.rs
, are siblings. Suppose students/mod.rs
needs to call a function defined in course.rs
. We can solve it in two approaches. First approach is using absolute path, like crate::course::get_course_details()
, which works for consuming any module with their absolute path. Second approach is using a relative path to the parent, utilizing the super
keyword, like super::course::get_course_details()
. The super
keyword refers to the parent module.
// students/mod.rs
pub mod enrollment;
pub fn choose_a_course() {
crate::course::get_course_details(); // absolute path
// rest of the code ...
}
// students/mod.rs
pub mod enrollment;
pub fn choose_a_course() {
super::course::get_course_details(); // relative path to the parent
// rest of the code ...
}
Consuming From or Into Nested Module
Finally, let’s address the more complex scenario of consuming modules across different levels of nesting. For instance we need to import a function from students/enrollment.rs
into course.rs
. Imagine there is a calculate_grade
function inside students/enrollment.rs
that needs to be imported.
// students/enrollment.rs
pub fn calculate_grade() {
// rest of the code ...
}
We can import this calculate_grade
function into course.rs
like this,
// course.rs
// other codes here...
pub fn course_result() {
super::students::enrollment::calculate_grade(); // relative path
// OR
crate::students::enrollment::calculate_grade(); // absolute path
}
The same principles apply when importing into a nested module; we can use absolute paths and, where possible, relative paths.
Conclusion
In this post I have given a practical introduction to Rust’s module system. For a deeper dive, I encourage you to explore the official Rust documentation and experiment with your own projects, Happy coding! 🎉 ✨
Cover photo by Artem Podrez.