DEV Community

Cover image for How The Rust Module System Works
Confidence Okoghenun
Confidence Okoghenun

Posted on • Edited on • Originally published at confidence.sh

How The Rust Module System Works

Modules in Rust can be somewhat confusing. They don't line up with what you're used to from other languages, and can be frustrating to work with. Adding to this, the official docs and other resources don't help that much. What if someone wrote an article where you can learn everything you need to know about modules in Rust. Wouldn't that be awesome?

In this article, I'll explain how Rust's module system works from top to bottom. I promise this article will be easy to understand and hope you'll find it helpful.

Let start

Modules In A Nutshell

Modules in traditional languages like JavaScript or C++ are pretty straightforward. Modules are created by exporting functions or classes from a file for reuse later on. In these languages, modules are linked with projects' file system, and thus are easier to grok. Unfortunately, that is not the case with Rust.

In Rust, modules behave differently and are not related to the file system. This is what makes them seem a bit harder to understand. On the bright side, this allows for a higher degree of flexibility for code organization. Thus, modules can contain functions, types (i.e. structs, enums), traits, implement blocks and more.

A module is created using the mod key word followed by the module name and braces {} containing its members. Since modules encapsulate code, members are private by default and are made public using the pub keyword. Here's an example of declaring an inline module (more inline and external modules later) in a main.rs file:

//main.rs
mod messages {
    pub fn say_hi() {
        println!("Hi there!");
    }
}

fn main() {
    messages::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

Fun fact, modules can contain other modules. Rust allows you to nest modules without restrictions. This is possible because modules work independently of the projects' file system. So you can go berserk and structure your program however you wish. Now that's power!

//main.rs
mod messages {
    pub mod english {
        pub fn say_hi() {
            println!("Hi there!");
        }
    }
}

fn main() {
    messages::english::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

This is mostly the fundamentals of the module system in Rust. But as you've guessed, there is more to using modules. Up next, we'll cover more concepts like external modules, visibility and paths. Ready? Let's go!

External Modules

While building ‘real' apps, you'll likely want your modules to live as standalone files and imported to parts of your program where they are needed. Rust offers this feature through external modules (i.e. modules in separate files). Thus, internal modules are inlined, while external modules are in separate files.

There are two ways to organize your code using external modules in Rust, and you can mix and match both options. The first option, which I'll call file modules, allows you to create modules in files with matching names. For instance, converting the last example into a file module requires creating a messages.rs file, then the module is used in main.rs. After completing this, the project file tree looks this way:

.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── messages.rs
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

Then, the content of each file:

//messages.rs
pub fn say_hi() {
    println!("Hi there!");
}

//main.rs
mod messages;

fn main() {
    messages::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

Notice there's no need to declare the module name using the mod keyword in messages.rs, because it's automatically inferred. Also in main.rs, the line mod messages;, informs the compiler that an external module called messages exists. Then the compiler searches for it, and includes during compilation.

Another option to create modules is what I'd like to call folder modules. To be honest, I prefer this method because it makes adding submodules much easier later on. Using folder modules involves creating a folder with the same name as the module, and placing its code in a mod.rs file within the folder. Here's an example:

.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── messages
       └── mod.rs
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

Then, the files look like:

//messages/mod.rs
pub fn say_hi() {
    println!("Hi there!");
}

//main.rs
mod messages;

fn main() {
    messages::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

Folder modules are like file modules, except the differences in the project's file system. But as we'll explore submodules next, you'll come to appreciate the beauty of folder modules. You'll also appreciate the flexibility of Rust's modules over modules in other languages.

Nested Modules With Submodules

A unique feature of Rust's modules is the ability to create nested modules. For large applications, this can go a long way in keeping things in good shape. There are two ways to create submodules based on the external module option used in your project.

Submodule meme

If you go with file modules, you can add submodules by creating a folder with the module's name, and adding any child or submodule with a matching file name in that folder. Let's see an example to make things clearer:

.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── messages.rs
    ├── messages
       ├── english.rs
       └── japanese.rs
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

The content of the files:

//messages.rs
pub mod english;
pub mod japanese;

//messages/english.rs
pub fn say_hi() {
    println!("Hi there!");
}

//messages/japanese.rs
pub fn say_hi() {
    println!("こんにちは");
}

//main.rs
mod messages;

fn main() {
    messages::english::say_hi();
    messages::japanese::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

Using folder submodules is not too different, but feels better organized. Here, you once again create a folder with a matching name, and each submodule as files in that folder. The only difference here is that mod.rs acts like a C header file, only containing the declaration of the submodules. Or in JavaScript, it's like an index.js file containing the re-exported modules. Here's an example:

.
├── Cargo.lock
├── Cargo.toml
└── src
    ├── messages
       ├── mod.rs
       ├── english.rs
       └── japanese.rs
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

The file contents:

//messages/mod.rs
pub mod english;
pub mod japanese;

//messages/english.rs
pub fn say_hi() {
    println!("Hi there!");
}

//messages/japanese.rs
pub fn say_hi() {
    println!("こんにちは");
}

//main.rs
mod messages;

fn main() {
    messages::english::say_hi();
    messages::japanese::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

There's no limit to the depth of nesting with submodules, and you can go as deep as you'd like. You can also mix and match the various options available, but it's best to stick with one for consistency. Now let's talk about module paths and how they work in Rust.

Module Paths, Imports, And Exports

At this point, you've learned quite a lot about modules. You're doing great!

One last unusual aspect about modules is how paths work. You've likely noticed that file paths i.e ./path/to/module.rs are not used here, and that's because modules are loosely linked with the file system. Rather, Rust introduces a somewhat new technique for accessing modules with module paths.

Module paths are built using the path separator operator i.e ::. Thus, accessing a module in Rust will always be in the form; path::to::module. Like file paths, module paths are relative and require the module starting the path to be in scope. Member functions in a module can be accessed directly using module paths like we've seen before, i.e. messages::japanese::say_hi();.

To access a path relative to the parent of the current module, you can use super::. This is analogous to ../ in plain-old file system path. Also, to build an absolute path starting at the crate root, you can use crate:: or just :: with a path to the module, i.e. ::path::to::module.

Let's see an example to put these concepts in context. Suppose we need to access members of one module from another, we could build a relative or absolute path, as the case may be. Here's an example to access the print utility function added to the messages module:

//main.rs
mod messages {
    pub mod utils {
        pub fn print(msg: &str) {
            println!("{}", msg);
        }
    }

    pub mod english {
        pub fn say_hi() {
            //relative path to print
            super::utils::print("Hi there!");
        }
        pub fn say_bye() {
            //absolute path to print
            crate::messages::utils::print("Bye bye!");
        }
    }
}

fn main() {
    messages::english::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

Imports are created with the use keyword. This is especially useful if a module is accessed more than once to keep, as it helps to keep the code concise. For example, we could import print with the use keyword to bring it in scope. Then, we can use it many times without needing to specify its path:

//main.rs
mod messages {
    pub mod utils {
        pub fn print(msg: &str) {
            println!("{}", msg);
        }
    }

    pub mod english {
        //import print once
        use super::utils::print;

        pub fn say_hi() {
            print("Hi there!"); //use here
        }
        pub fn say_bye() {
            print("Bye bye!"); //and here
        }
    }
}

fn main() {
    messages::english::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

Just like in other languages, imported modules can also be renamed using the as keyword, i.e. use super::utils::print as msg_print. This is useful in situations where members of two different modules have the same name. Using as, you can easily rename one import to avoid the name collision.

Lastly, modules can be re-exported with pub use declarations. This is used to shorten the module path or create shorter aliases to frequently used modules. Let's look at an example of pub use in action. As we do, note how it's used to shorten the path to say_hi without diminishing the original path:

//main.rs
mod messages {
    pub mod english {
        pub mod greetings {
            pub fn say_hi() {
                println!("Hi there!");
            }
        }
    }

    //re-export deeply nested path to module root
    pub use english::greetings::say_hi;
}

fn main() {
    messages::say_hi(); //shorter resulting path
    messages::english::greetings::say_hi(); //old path still works
}
Enter fullscreen mode Exit fullscreen mode

Awesome! We've seen how module paths and exports works. Now let's wrap up the article by learning about visibility.

Visibility

Visibility refers to parts of a module that can be accessed from outside. By default, all modules and their respective members are private in Rust. Optionally, they are made public using the pub keyword. This controls the visibility outside a module, as only public members are accessible. However, all members are always accessible within the module or its submodules.

Visibility

Here's an example to illustrate this. In the code below, print is a private member of the english module, and is accessible from say_hi, because it's in the greetings submodule. However, print is not accessible not from say_bye in japanese even though the module path is correct. This is because print is not accessible from modules exterior to english. Here's the code:

//main.rs
mod messages {
    pub mod english {
        //private member function
        fn print(msg: &str) {
            println!("{}", msg);
        }
        pub mod greetings {
            pub fn say_hi() {
                //child of print's parent. visible ✅
                print("Hi there!");
            }
        }
    }
    pub mod japanese {
        pub fn say_bye() {
            //correct module path but not visible ❌
            super::english::print("こんにちは");
        }
    }
}

fn main() {
    messages::english::greetings::say_hi();
}
Enter fullscreen mode Exit fullscreen mode

This program doesn't compile because print is private and unreachable from japanese. This can be easily fixed by making print public using pub.

One more thing. In modules, all fields in a struct are private by default. To make the fields accessible outside the origin module, each field should be pub'ed as needed. Here's an example:

pub struct User {
    pub name: String,
    pub email: String,
    active: bool, //private field
    login_count: u64, //private field
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Rust's module system may be a bit confusing at first, but the concepts discuss here should help clear things up. Still have questions? That's okay, let's have a discussion in the comments.

If you enjoy all things Rust, follow me on Twitter. Cheers, have a good one!

Top comments (0)