Rust's Module System
We'll learn to manage growing projects using package, crates and modules. There is a requirement for organization and encapsulation of code as it grows in size.
Rust has a module system, starting with
- package: cargo new <name>
creates a new package, and package stores crates.
- crates: crates could either be binary crate, something which is executable or a library crate which is code that can be used by other programs. Crates contains modules.
- modules: modules allows us to organize a chunk of code and control the privacy rules.
If we wanted to create a module like Authentication, the internal function could remain private but expose one login method.
Rust also has workspaces meant for very large projects and allow us to store interrelated packages inside the workspace.
Managing Growing Projects with Packages, Crates, and Modules
Packages and Crates
Let's start with creating a new package.
In the auto-generated Cargo.toml
we can see that there are no crates defined, which doesn't mean our package dosn't have any crate.
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
Well, we actually do have one binary crate, for the main.rs
. Rust follows the convention that if you have main.rs
then a binary crate with the same name as your package will be automatically created and main.rs
will be the crate root, which also makes the root module.
The crate root is the source file that the rust compiler starts at when building your crate.
There is also a similar convention for library crate, for a file that might exist in src
directory. Say, something like lib.rs
.
lib.rs
is defined in the root of our src
directory, then rust will automatically create a library crate with the same name as your package, and lib.rs
will be the crate root.
Which means our my-project
has two crates, one binary and other a library crate.
Crate Rules
- A package must have at least one crate.
- A package could have zero library crate or one library crate.
- A package can have any number of binary crates.
So any other file like another_file.rs
will represent another binary crate.
Defining Modules
Let's start by creating a new package restaurant
that contain a library crate.
In which lib.rs
we automatically get a test module something like this, which we don't need right now, so delete it.
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Our goal with new package is to create a library to help run a restaurant. Think about restaurant as two parts, front of house which serves customers, back of house where food is made.
// lib.rs
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
We start with a module defined using mod
keyword called front_of_house
. Inside which we have two more module called hosting
and serving
. Modules can contain other modules, enums, structs, constants, traits and so on inside of them.
So our module tree looks something like this:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
At top we have a module called crate
that gets created by default for our crate root which is lib.rs
.
Paths
A good analogy for module tree is to thinking about them like the folder/directory tree on computer. If we wanted to reference a file inside a directory, in the same way we'd want to reference a item in a module, we'd be required to specify a path to that function.
Check the code below, a simplied front_of_house
module inside of which we have hosting
module which declares add_to_waitlist()
function.
We want to call this function in eat_at_restaurant()
, we need to specify the path (specified using identifier separated by double colon) which could either be:
- Absolute path
- Relative path: start from current module
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
Module Privacy Rules
Our code above will show error for hosting
because mod hosting
is private.
Hiding implementation details
This is because in Rust by default a child and module and everything inside of it is private from the perspective of the parent module.
Child modules can see anything that's defined in their parent module.
If we want to expose any entity inside our module for public access, include pub
keyword in front of it.
mod front_of_house {
pub mod hosting {
// this function will be private from `hosting`'s perspective
pub fn add_to_waitlist() {}
}
}
Let's look at another example with relative paths using super
keyword.
// lib.rs
fn server_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order(); // can call because defined in same module
super::serve_order(); // super to reference parent module, i.e., crate
}
fn cook_order() {}
}
Privacy rules with Structs.
In this example we have module called back_of_house
storing a Struct Breakfast
and an impl
block implementing as associated function summer()
.
mod back_of_house {
struct Breakfast {
toast: String,
seasonal_fruit: String,
}
impl Breakfast {
fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("wheat");
}
But this fails, because Breakfast
as well as summer()
associated function are both private by default.
To fix, once again add pub
keyword before each one.
mod back_of_house {
pub struct Breakfast {
toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("wheat");
}
If we want to change the toast of meal
of type Breakfast
we'll get an error, because field itself are also private by default.
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("wheat");
meal.toast = String::from("Rye"); // error!
}
Again add pub
keyword infront of toast
field.
We can't build Breakfast
struct directly because it contains a private field, which is inaccessible.
The same privacy rules also applies to Enums.
The Use Keyword
Here specifying the full path of a function (as in below example calling add_to_waitlist()
called 3 three times) isn't pretty or ideal.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_watilist();
}
To deal with this problem, Rust provides us the use
keyword.
use
keyword allow us to bring a path into a scope.
Let's bring `hosting module into the scope:
use crate::front_of_house::hosting;
// or use relative path; use only `self` to reference current module.
use self::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
Idiomatic use of paths
- It's idiomatic to bring the function's parent module into scope, since that would allow us to minimize the path we've to specify but we're also making it the function used isn't local.
-
If we're bringing Enums, Structs or other items into scope, it's idiomatic to specify the full path when using
use
keyword.An exception is that if you are bringing two items from different modules having same name, then bring parent module into scope so that names don't conflict.
As an example in the below example both function return a Result
type defined in two different modules, without conflicting:
Another way could be to rename one of the Result type when bringing it into scope.
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {}
fn function2() -> IoResult<()> {}
Back to Restaurant example, let's talk about re-exporting.
What if we wanted to make the function add_to_waitlist()
available to any external code.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
// currently only `eat_at_restaurant()` is available outside of this file because of `pub` keyword
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
To re-export the hosting
module, add pub
keyword infront of use
statement. This allows external code to reference the hosting
module as well as use it at the same time.
use
keyword also allows us to bring in items from external dependencies as defined in Cargo.toml
into a scope.
For example, let's add a rand
dependencies to Cargo.toml
.
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rand = "0.5.5"
Then we can bring Rng
trait into scope using use
keyword in lib.rs
use rand::Rng;
mod front_of_house {
// ...
}
pub fn eat_at_restaurant() {
let secret_number = rand::thread_rng().gen_range(1..101);
...
}
And we can bring multiple items using Nested paths:
Instead of bringing io
and Write
into scope both in io
module like this:
we can write it as:
The Glob Operator
Bring all the public items underneath a module into scope.
Bringing all public items from io
into scope.
Modules in Separate Files
As programs grow module get's large in size, in that we would want to move module definition into another file.
Let's move front_of_house
to new file:
and in lib.rs
we'll write something like below which tells Rust to define module front_of_house
here but get contents from a different file/folder named same as front_of_house
:
Let's extract the definition of hosting
module into separate file, making the whole module a directory, which can consist of different files.
You can either delete src/front_of_house.rs
and add src/front_of_house/mod.rs
with following content.
Or keep src/front_of_house.rs
and change it's content to: