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.

Example 1

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

Understanding Rust modules with Practical Examples
6.90 GEEK