Rust Learning: Modules
Overview
Rust modules is one of those things where I thought I learned it but it keeps giving me surprises.
I was recently contributing to an inner source rust project and got tripped by modules and realized that I don’t actually understand them.
So here I am trying to learn again and write things down.
I’ll go over the basics which explains the concept and syntax. Then I’ll show some idioms people use.
Might not be exhaustive but this is aligning with my understanding.
tldr
- Rust’s module tree is explicit, you have to write it yourself.
modis the keyword for you to build the module tree.pub mod abcdeclares moduleabcand makes it public.useis the keyword to “import” other modules, it creates shortcuts.pub use abccreates shortcuts and shares that with users of your module.- To refer something in a sibling module, you need to use relative path
super::sibling_nameor absolute pathscrate::path_to_sibling.
The Basics
Module Tree
I believe this is a rust concept, it means what module structure(tree) does your program has.
This isn’t a thing in languages like Python because in those languages the module tree maps to the file system.
Say in python I want to create my math package with four operators in it, I would do the following in mymath.py
mymath.py
And in other places they could simply import mymath and use mymath.add() or from mymath import add then just use add()
In rust having mymath.rs is not enough, you need to explicitly tell the compiler you have a module.
mod keyword - to declare or define modules in the module tree
mod keyword is the keyword to declare or define modules in rust.
Note that declare and define a module are slightly different.
When using the inline syntax
moddefines the module, when using the file+folder waymoddeclares the module.
To define a module, there are three ways.
(1) Inline
src/main.rs
In this way we defined and declared the module at the same time.
(2) File and Directory
The above feels like good tool to do ad-hoc things. Almost feels like in C++ where you open a namespace anywhere and starts adding things in it. A more natural way IMO would be using files and directories.
In rust there exists two ways, one of them being “legacy”, and the other one is the recommended way.
(2.1) [legacy] mod.rs + module_name/
You simply create a folder and name the folder name to be the module name.
Then you would need to have a mod.rs in the folder.
You would use mod.rs to declare which modules are available to the outside world.
It could also re-export some internal modules to a different path.
We’ll go over re-export in the use section.
folder structure
# folder structure
.
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
└── my_module
├── mod.rs
└── operations.rs
3 directories, 5 files
src/my_module/mod.rs
src/my_module/operations.rs
src/main.rs
(2.2) [recommended] module_name.rs + module_name/
The above is nice but there is one catch. In a big project there would be a lot of mod.rs files.
Later a new way was introduced, essentially pulling mod.rs out of the folder and name it same as the folder (module name).
folder structure
# folder structure
.
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
├── my_module
│ └── operations.rs
└── my_module.rs
3 directories, 5 files
I essentially pulled mod.rs out and renamed it.
The rest is the same but for completeness sake I’ll put them below.
src/my_module.rs
src/my_module/operations.rs
src/main.rs
Visibility: public or private
You might have noticed that in the code example above there are some pub keywords here and there.
Everything in rust is defaulted to be private. Adding pub simply makes the declared module public and can be accessed outside.
Every module can have child modules, and in order to use something from the child module, the child module must make it visible for the parent.
Let’s look at a concrete example.
Say we have my_module and inside my_module we have operations, let’s say we need some utility functions that are only available for operations but we don’t want it to be exposed to users of my_module.
We would (1) create a sub module under operations, mark the functions as pub and then (2) declare the module in places we need to use. and (3) do not expose it in operations.rs
.
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
├── my_module
│ ├── operations
│ │ └── utils.rs
│ └── operations.rs
└── my_module.rs
4 directories, 6 files
I’ll only pick the interesting files here
src/my_module/operations.rs
src/my_module/operations/utils.rs
The pub on utility_func in utils.rs is required, otherwise the function won’t be accessible in operations.rs.
The mod utils; in operations.rs is important, because it brought utils into the module tree. Mounted utils as a child of operations.
Note that in main.rs there is no access to utils::utility_func because it was not made public in operations.rs.
To make utils::utility_func available in main.rs, we could write pub mod utils instead of mod utils in operations.rs. The line pub mod utils does two things.
- It mounts the module to the module tree.
- It makes the module public for whoever is mounting the current module. (operations) in this case.
It can be confusing because sometimes when slapping on the pub keyword, a single line does two things.
There are also other fine grained control of visibility like pub(crate), pub(super) but we’ll go over them later.
Using modules from sibling
There is a tool called cargo modules, using that tool you can check the module tree.
Running cargo modules structure at the root of the project shows the following output
crate modules_playground
├── fn main: pub(crate)
└── mod my_module: pub(crate)
└── mod operations: pub
├── fn add: pub
└── mod utils: pub(self)
└── fn utility_func: pub
The utils module we created is under operations, what if I want to have a utils module under my_module and share it across my_module?
In order to do this, let’s define the utils module under my_module by creating a utils.rs file inside my_module/ directory. Then in my_module.rs we need to declare it with mod utils;
.
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
├── my_module
│ ├── operations.rs
│ └── utils.rs
└── my_module.rs
3 directories, 6 files
src/my_module/utils.rs
src/my_module.rs
And in order to use it from operations.rs, you need to either call it with a relative path, or call it with an absolute path.
To call it with a relative path, operations needs to go up to my_module then go down to utils.
To call it with an absolute path, operations can just specify the path from the root.
Let’s see both in action src/my_module/operations.rs
Visibility: all pub keywords
So now we know that the mod keyword is to declare/define modules.
And the pub keywork is to tweak the visibility.
There are some variants of pub, I’ll list them below
pub, public everywherepub(crate), public in your current cratepub(super), public to your parent(no grandparents) and sibling and kids of siblings(nephews/nieces)pub(in path::to::module), public to the specific module pathpub(self)or ignored, public to yourself, essentially private
pub(super) granting access to nephews/nieces seems strange but it’s because of the following rule.
When you grant visibility to a module, you automatically grant visibility to every single module nested inside of it.
mod Summary
So we know that mod keyword declares/defines a module in the module tree. And pub keyword can change the visibility of the module. If you write them in the same line it does two things with one line.
pub mod abc; means declaring abc in the current module and make it public to whoever uses the current module.
use Keyword
In the last example above (calling sibling modules), we saw how to call the function with different paths
src/my_module/operations.rs
However it would be tedious to keep writing the path.
use keyword could be used to shorten the path. It feels similar like import in Python, or use in C++.
It’s easier to think that use creates shortcuts.
We could use till the module and call the function like utils::utility_func(). Similar to import utils in Python.
src/my_module/operations.rs
Or we could use the full path and call utility_func() without utils::. Similar to from utils import utility_func in Python.
src/my_module/operations.rs
pub use, aka the Facade Pattern
pub use is a way to share the shortcut use created. It’s known as re-export.
Let’s look at a deeply nested module tree example.
.
├── Cargo.lock
├── Cargo.toml
└── src
├── main.rs
├── my_module
│ ├── aaa
│ │ └── bbb.rs
│ └── aaa.rs
└── my_module.rs
4 directories, 6 files
crate modules_playground
├── fn main: pub(crate)
└── mod my_module: pub(crate)
└── mod aaa: pub
└── mod bbb: pub
└── fn bbb_func: pub
Use this as an exercise to see if you can fill out what is needed in each file to create the module tree like this.
src/my_module.rs
src/my_module/aaa.rs
src/my_module/aaa/bbb.rs
src/main.rs
In main.rs, it is clear that bbb_func() is deeply nested, we could use pub use inside my_module.rs to create a shortcut and make that shortcut available, so clients (main.rs) don’t really need to know about this aaa::bbb:: module tree structure.
src/my_module.rs
src/main.rs
If we wanted we could go one step further to create a shortcut of bbb_func on my_module.
src/my_module.rs
src/main.rs
The Idioms
The pub use above is one of the commonly used idiom/pattern. Let’s go over a few that I saw. This might not be exhaustive but you gotta start somewhere.
using type vs using functions
In my understanding, the convention for functions is to have the module name, the convention for types is to import the full path.
Prelude
Some packages have stuff that most people would like to use anyways, so they export them in a prelude module and users would use *. Examples like use rayon::prelude::*;, use bevy::prelude::*;, use std::io::prelude::*;
Facade Pattern
We already covered this when we go over pub use. This is essentially creating shortcuts and share them with the users of your module.
The pattern I saw
I saw this code when working on the inner source project.
I was super confused then I learned that the reason the modules are structured like this, is to give clients the ability to either refer Type1 or refer it with my_module::Type1.
The gotcha I got was the last line didn’t have self::, so it had ambiguity where rust didn’t know which my_module it is referring to.
Because the outer sibling module and the nested module share the same name, omitting self:: causes ambiguity for the compiler over which my_module is being targeted. Adding self:: explicitly tells the compiler to look at the local module in the current scope (the outer one), fixing the ambiguity.
Rust Modules are Completely Closed
Rust modules feels like C++’s namespaces (probably because of the use keyword).
C++ namespaces could be “opened” anywhere and have things added to it.
It then begs the question, can rust modules be “opened” anywhere and have things added to it?
No, modules are completely closed in rust.