Rust’s module system is surprisingly confusing and causes a lot of frustration for beginners.
In this post, I’ll explain the module system using practical examples so you get a clear understanding of how it works and can immediately start applying this in your projects.
Since Rust’s module system is quite unique, I request the reader to read this post with an open mind and resist comparing it with how modules work in other languages.
Let’s use this file structure to simulate a real world project:
my_project
├── Cargo.toml
└─┬ src
├── main.rs
├── config.rs
├─┬ routes
│ ├── health_route.rs
│ └── user_route.rs
└─┬ models
└── user_model.rs
These are the different ways we should be able to consume our modules:
These 3 examples should be sufficient to explain how Rust’s module system works.
Let’s start with the first example - importing config.rs
in main.rs
.
// main.rs
fn main() {
println!("main");
}
// config.rs
fn print_config() {
println!("config");
}
The first mistake that everyone makes is just because we have files like config.rs
, health_route.rs
etc, we think that these files are modules
and we can import them from other files.
Here’s what we see (file system tree) and what the compiler sees (module tree):
Surprisingly, the compiler only sees the crate
module which is our main.rs
file. This is because we need to explicitly build the module tree in Rust - there’s no implicit mapping between file system tree to module tree.
We need to explicitly build the module tree in Rust, there’s no implicit mapping to file system
To add a file to the module tree, we need to declare that file as a submodule using the mod
keyword. The next thing that confuses people is that you would assume we declare a file as module in the same file. But we need to declare this in a different file! Since we only have main.rs
in the module tree, let’s declare config.rs
as a submodule in main.rs
.
The mod keyword declares a submodule
The mod
keyword has this syntax:
mod my_module;
Here, the compiler looks for my_module.rs
or my_module/mod.rs
in the same directory.
my_project
├── Cargo.toml
└─┬ src
├── main.rs
└── my_module.rs
or
my_project
├── Cargo.toml
└─┬ src
├── main.rs
└─┬ my_module
└── mod.rs
Since main.rs
and config.rs
are in the same directory, let’s declare the config module as follows:
// main.rs
+ mod config;
fn main() {
+ config::print_config();
println!("main");
}
// config.rs
fn print_config() {
println!("config");
}
We’re accessing the print_config
function using the ::
syntax.
Here’s how the module tree looks like:
We’ve successfully declared the config
module! But this is not sufficient to be able to call the print_config
function inside config.rs
. Almost everything in Rust is private by default, we need to make the function public using the pub
keyword:
The pub keyword makes things public
// main.rs
mod config;
fn main() {
config::print_config();
println!("main");
}
// config.rs
- fn print_config() {
+ pub fn print_config() {
println!("config");
}
Now, this works. We’ve successfully called a function defined in a different file!
#rust #developer