loading...
Cover image for Intro to Rust Modules

Intro to Rust Modules

stevepryde profile image Steve Pryde ・7 min read

The Rust module system can be very confusing and difficult to understand when you are just starting out learning Rust. Something as simple as just putting a function in another file and importing it into your main.rs can send you on a frustrating search and leave you with even more questions.

In this article I will share some info that I hope will be helpful in understanding how to make better use of the module system and perhaps even start to embrace the functionality it gives you.

Why not just import by file path?

I'm sure this is the approach many of us tried first and we were disappointed when it didn't work.

As an example, let's imagine we have some file called cat.rs and it contains the function meow():

fn meow() {
    println!("Meow, Meow!");
}

And then we try to call meow() from main.rs like so:

fn main() {
    meow();
}

But Rust says no:

error[E0425]: cannot find function `meow` in this scope
 --> src/main.rs:2:5
  |
2 |     meow();
  |     ^^^^ not found in this scope

Ok. We forgot to make meow() public using the pub keyword, so we will fix that now and try again. Nope, same error.

What we might try next is something like this:

use cat;

fn main() {
    cat::meow();
}

Now we get this error:

error[E0432]: unresolved import `cat`
 --> src/main.rs:1:5
  |
1 | use cat;
  |     ^^^ no `cat` external crate

The mistake in the above code is that it assumes there is a 1 to 1 mapping between module name and our directory structure. However, Rust does not work like that, and I think this is the key point that helps in understanding how Rust modules work.

Rust's module paths are not tied directly to the filesystem. Rather we need to tell Rust a little more about the module structure before we can use our modules in our code. That is, think of the module structure as its own thing independent of the file system layout. First we need to tell Rust where each file belongs in the module hierarchy, and then we can import our module using the "module path" rather than the filesystem path.

This can seem cumbersome at first, but you will come to understand the reasons why it works this way.

To demonstrate why Rust doesn't just use file paths as the module path, let's look at an example with multiple modules in one file, like this main.rs:

mod cat {
    pub fn meow() {
        crate::dog::woof();
    }
}

mod dog {
    pub fn woof() {
        println!("Woof, Woof!");
    }
}

fn main() {
    cat::meow();
}

Obviously using separate modules for this is overkill but it demonstrates the way modules work.

Note that we need to use the pub keyword to allow each function to be visible to other modules (and you can also make things private to a module by omitting the pub keyword).

This will compile. And when we run it we get:

Woof, Woof!

So we can see that mod cat created a kind of module path with the function meow() nested below it, even though it is in the same file. And the module itself is nested below a global namespace called crate.

In this way we can declare different module namespaces in a much more flexible way than if we could only use file paths. For a simple example like this you may think it is unnecessarily complex but when you are writing a library and you want to provide a nice import structure for your API without needing to match that exact structure in your filesystem, this is a really nice feature. If you have ever used a library that let you do: use somelib::prelude::*; that is one of the benefits.

What about different files?

Ok, so hopefully multiple modules in 1 file makes sense. What about importing functions from another file?

Let's return to our first example, with meow() declared in another file, named cat.rs.

The change we need to make within main.rs is to first tell Rust about the module cat.

The way we do this is by adding the following to our main.rs:

mod cat;

That tells Rust to look for either a cat.rs in the same directory OR alternatively a directory named cat with a mod.rs file within it (but you cannot have both a cat.rs file and a cat directory). We will get to directories shortly.

In our case we have cat.rs in the same directory, so now in our main.rs we can call the function within that module like so:

mod cat;

fn main() {
    cat::meow();
}

And that works!

What about directories?

Previously I mentioned that mod cat; could point to a directory named cat containing a file called mod.rs.

Let's create this directory for our example, and then move cat.rs into that directory. Actually we will also rename cat.rs to kitten.rs to disambiguate (it's in a "child" module hehe).

So now we have a main.rs and a directory named cat containing two files, mod.rs and kitten.rs.

We could just put our meow() function in mod.rs and that would work just fine. However let's show how to import from another file within that directory.

In mod.rs we now need to tell Rust about kitten.rs, like this:

pub mod kitten;

(Note the pub keyword which makes the kitten module visible to the parent module as well)

But now our main() would need to call meow() like this:

mod cat;

fn main() {
    cat::kitten::meow();
}

What if instead we wanted to use the original import path in main(), while keeping our new file structure? Can we do that?

Yes we can! This is where Rust's module system hopefully starts to make more sense.

Try this in mod.rs:

mod kitten;
pub use kitten::meow;

And now we can once again call cat::meow() from within our main() function.

What this does is within mod.rs we tell Rust about another module, called kitten (which here points to kitten.rs in the same directory as mod.rs), which then becomes visible to the current module scope. But without the pub keyword, the nested module is invisible to the parent. Rather than make the entire module visible, we can import the meow() function from that module via use and we can make only this import public (visible to the parent module) with pub use.

We could even change the name of the function when re-exporting it, like this, in mod.rs:

mod kitten;
pub use kitten::meow as woof;

and now our main() would need to look like this:

fn main() {
    cat::woof();
}

I hope you are starting to see the flexibility that the Rust module system gives you over your entire namespace hierarchy. You can organise your files in a way that is convenient for you, and you can export your structs and modules in the way that makes more sense for the caller.

Sometimes these will be the same, and sometimes they won't. Rust lets you choose.

Declaring the module hierarchy in one place

Putting module declarations in mod.rs files and so on is ok, but I personally think it can start to lead to too many module declarations scattered throughout the codebase and it can become difficult to follow.

In my own code I prefer to declare the entire module structure in main.rs (or lib.rs for library crates), which avoids the need for any mod.rs files while offering all of the same functionality.

For our above example, we can have our directory named cat and within it just the file kitten.rs (no mod.rs this time), and now our main.rs can look like this:

mod cat {
    mod kitten;
    pub use kitten::meow;
}

fn main() {
    cat::meow();
}

When I discovered this, that was the moment when I realised how powerful the Rust module system really is. This lets me describe my module structure all in one place, and then my whole crate can just refer to this structure to import things.

For example, let's add a new directory below cat called activities and containing a file called play.rs.
Here is play.rs:

pub struct CatToy {
    name: String
}

impl CatToy {
    pub fn new(name: &str) -> Self {
        Self { name: name.to_string() }
    }

    pub fn fetch(&self) {
        println!("Fetch the {}", self.name);
    }
}

We can describe this layout in main.rs like so:

mod cat {
    // We still have our imports from before.
    mod kitten;
    pub use kitten::meow;

    // And now we describe the additional structure.
    pub mod activities {
        pub mod play;
    }
}

fn main() {
    let ball = cat::activities::play::CatToy::new("ball");
    ball.fetch();
}

This is ok, but our import path is a bit long. We want to keep our files organised but we don't want the callers of these functions to have to worry about our directory structure. Or perhaps we want to modify our directory structure without modifying the import paths (so users of your crate don't need to update their code too).

So we can shorten the import path like this:

mod cat {
    mod kitten;
    pub use kitten::meow;

    mod activities {
        pub mod play;
    }

    pub use activities::play::*;
}

fn main() {
    let ball = cat::CatToy::new("ball");
    ball.fetch();
}

So you can see this greatly simplifies the use of the code for the caller.

One last thing (well, several)

If you're importing (with use) something into another module outside of main.rs, you would need to add crate:: to the start of the module path.

For example, if we wanted to use CatToy from another file:

use crate::cat::CatToy;

pub fn fetch_ball() {
    let ball = CatToy::new("ball");
    ball.fetch();
}

We didn't need to do that in the above examples because the main() function was already at the base level crate scope and all of the other modules were children of that scope.

Finally, if you just want to import and use a module somewhere without needing to re-export it to the parent module, you don't need pub use. Just use crate::module::path; will work fine. Only use pub when you need to make something visible to the parent scope.

Further reading

Hopefully this article has helped to make the Rust module system a little easier to understand.

There is a lot more that I didn't cover.

If you want to read more about this, I highly recommend the documentation at Rust By Example.

Also the Official Rust Book has a good section on this as well.

I found a lot of the official documentation difficult to understand at first, but once I understood the basics of modules I was able to go back and re-read them and everything started to fall into place. It still took a lot of practice to get my head around it, but now I find it really nice and very powerful.

Discussion

pic
Editor guide