When I started learning Rust, the module system did not at first seem to be a shining beacon of intuitive design. The Rust documentation is phenomenal, but there are definitely some areas that I found difficult to follow; this being one such topic. So I thought I might take a stab at writing up a guide that I think would have helped me through the awkward growing pains a bit quicker.
*DISCLAIMER*
This article is not intended to explain all of the intricacies of the Rust module system, nor is it for demonstrating proper project structure. It’s simply a quick overview of the actual mechanics around using modules in Rust.
Start your stop watch….now!
The View at 30,000 Feet
There are three general ways to declare a module in rust.
- Embedded in an existing source file.
- As a separate file next to your
lib.rs
ormain.rs
- As a separate folder leveraging a
mod.rs
file.
With flexibility often comes complexity and confusion, so let’s unpack each of those.
Embedded Modules
These are modules that you would create to break up certain parts of your file into different namespaces. It looks something like this:
In case it wasn’t obvious before, modules are used for isolating and packaging up some code for use elsewhere. As the comments explain, only public members of the module are available to external callers.
In my somewhat limited experience writing Rust, this seems to be one of the least useful forms modules take. That said, it’s still important to know and could certainly be the right tool depending on what you’re building.
Module Files
These are typically modules that you want to separate from the rest of your code, but don’t contain any sub-modules of their own (although there is no such constraint*). They look something like this:
If you’re paying attention, you’ll notice that the code hasn’t really changed at all. We’ve moved what was in the old mod
block into its own file named doorman.rs
. The naming is important here! The mod
declaration in main.rs
lets the rust compiler know that it needs to look for a file specifically named doorman.rs
and make that code available under the doorman::
namespace.
Module Folders
These are typically modules that have sub-modules within them. If you imagine having multiple, smaller libraries that make up a larger project, this is likely the approach to take. Here’s what the simplest example looks like:
Notice a difference? It’s subtle, right? The only change is we’ve moved src/doorman.rs
to src/doorman/mod.rs
. So when you have a module declaration like mod doorman;
, the Rust compiler is looking for either a doorman.rs
or a doorman/mod.rs
file in the same directory as the declaration.
Admittedly, this particular example might leave you asking “What’s the point? Why not just always use doorman.rs
?”.
Back when we were looking at the module files example, our program was made up of a main “module”, main.rs
, and a doorman module, doorman.rs
. The same exact principles can be applied to a module itself. This becomes useful when organizing a big, complex project into smaller, bite-size pieces. In keeping with the doorman example, we could refactor into something like this:
Now lets remember what we’ve learned so far and see if we can figure out what the Rust compiler is going to do with this.
- The
mod doorman;
declaration inmain.rs
tells the Rust compiler that there’s a module nameddoorman
somewhere in thesrc
directory. - The compiler will find the
doorman
directory and themod.rs
file inside of it, loading that file as thedoorman
module. - Inside
mod.rs
we find two more mod declarations,pub mod greeter;
andpub mod closer;
. The compiler again looks for modules, this time namedgreeter
andcloser
, but in thedoorman
directory. It’s also important to note that the declarations use thepub
keyword. This exposes both thegreeter
and thecloser
modules as accessible outside of thedoorman
module. - The files
src/doorman/greeter.rs
andsrc/doorman/closer.rs
are found and loaded as their respective modules. Making both thegreet()
andclose()
functions accessible. - Finally, from our
main()
function, we can greet the user withdoorman::greeter::greet(name);
and say goodbye to them withdoorman::closer::close(name);
Final Thoughts
That’s it! Now you should be able to break that giant main.rs
into dozens of little modules. Or not! That’s the power of the module system, once you’ve wrapped your head around it. As a programmer, you’re given the freedom to choose the project structure and levels of indirection that make sense for your program. But at the same time, just about every Rust project you come across will be structured predictably (albeit in some combination of the three methods we discussed today). There also isn’t any additional syntax you have to include in your files that tell Rust what module or package they should be in. It’s all driven by convention.
It’s also important to realize that none of this even touches on crates, which is another packaging/isolation construct that works at an even higher level than modules. Crates, however, are much more similar in principle to the code packaging you’re probably already familiar with from other languages. So I think you’ll find them much easier to grok if you haven’t already.
*There really aren’t any constraints to when you should use one form versus another. It’s entirely up to you as the programmer.
For more detailed information:
https://doc.rust-lang.org/1.1.0/book/crates-and-modules.html