Intro
As projects grow, you’ll need to refactor them into separate files and modules, sometimes into separate packages, for organization, maintainability, and reusability.
In this chapter we’ll be going over some of the ways we structure Rust projects.
- Packages: A Cargo feature that lets you build, test, and share crates
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of paths
- Paths: A way of naming an item, such as a struct, function, or module
Packages and Crates
Crates
This is the most atomic amount of Rust code that the compiler would consider at any time. Even running a single .rs file using rustc instead of Cargo, the compiler would treat that file as a crate.
Crates can be a binary crate or a library crate.
Binary Crates
These a re programs that can be compiled and run and runnable, like a command line program or a server.
They must have a main function to act as the entry way when the program runs.
// This is what you know thus far, it's the typical Rust program.
Library Crates
Don’t have main functions because they don’t compile to run. Their functionality is meant to be shared (and used) by other programs (crates) that do run.
// In online discourse, when the community says "crate", most of the time they mean library crates.
Packages
A package is a bundle of one or more crates that provide a set of functionality.
Every package contains a Cargo.toml file that describes how to build those crates.
For example, Cargo itself is actually a package that has a binary for the CLI tool used to build projects, it also contains the library crate the binary crate requires.
Package Composition
Packages can contain as many binary crates as we’d like but only one library crate at a time.
Cargo’s Convention
Whenever we make a project using Cargo, it relies on the src/main.rs being there to be the entry point for execution. There’s nothing of this mentioned in Cargo.toml.
Likewise, it knows that if the package’s directory has a folder src/lib.rs then that package contains a library crate with the same name as the package and that src/lib.rs is the library crate’s root.
Then Cargo will pass the crate root files to rustc to build the binary or library.
“If a package contains
src/main.rsandsrc/lib.rs, it has two crates: a binary and a library, both with the same name as the package. A package can have multiple binary crates by placing files in thesrc/bindirectory: each file will be a separate binary crate.”
Defining Modules, Privacy, and Scope
There are 3 important keywords to know.
uselet’s you bring a path into scope, for example importing another module’s functions into the one you’re working in.pubsets members of a module as public to other modules. Everything’s private by default.aswhich allows assigning aliases to modules. Used withusekeyword.
Modules
Crate’s root: when compiling a crate, the compiler will search for the root, as mentioned before it’s either src/main.rs or src/lib.rs to find the code it needs to compile.
Declaring Modules
Modules can be declared in the root files using the mod mod_name keyword.
for example: mod Car;
Compiler will search for the module’s code in the following locations:
- Inline, within curly brackets that replace the semicolon following
mod garden
mod Car{
// Car's code and functions...
}- In the file
src/car.rs - In the file
src/car/mod.rs
Sub-Modules
Any module can contain any number of sub-modules.
For example, our car can have a mod engine;
And just like with modules, the compiler will search in:
- Inline, directly following
mod engine {}; - In the file
src/car/engine.rs - In the file
src/car/engine/mod.rs
Paths to module code
Once a mod is declared and if the privacy rules allow, we can simply access the code using the :: operator to access the namespace and use its functions.
Privacy
A Module’s code is private from its parent modules by default unless declared it’s declared using pub mod instead of mod.
The same goes for members of a public module, to make functions public their declarations need to be preceded using pub as well.
Using Modules
To use the modules without repetitively using their namespaces, the use keywords allows us to import them once int a module.
All together
Created a binary crate named garage that illustrates these rules. The crate’s directory, also named “garage”, contains these files and directories:
garage
├── Cargo.lock
├── Cargo.toml
└── src
├── car
│ └── engine.rs
├── car.rs
└── main.rsThe crate root file in this case is src/main.rs
Example contents:
use crate::car::engine::V8;
pub mod car; // line tells the compiler to include the code
fn main() {
let engine = V8 {};
println!("I'm revving my {engine:?}!");
}Grouping Code into Modules
The main reason we break things down in programming, is organization. Organized code is readable, modular, reusable, and maintainable. It also allows us to control privacy, exposing only the APIs we need, i.e. Encapsulation.
Creating New Library
To create a new library run cargo new <lib_name> --lib.
Then navigate to src/lib.rs to define the modules and their function signatures; this serves as the API.
In the restaurant example they’re demonstrating, this would be the exposed front-house:
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() {}
}
}Modules let us group related (functionally or semantically) items together.
A Module can define more than just functions, it can define structs, enums, constants, and traits.
The src/lib.rs and src/main.rs modules are called crate roots because they create a module named crate that lives in the root of the crate’s module structure.
If this were to be represented using a tree it’d look like this:
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
Notice how some modules are nested within others, whereas others are siblings sharing the same parent module.
If module A is contained inside module B → then module A is the child of module B and that module B is the parent of module A.
The entire module tree is under the implicitly created, module crate, which is the parent of all the modules.
Referring to Items in the Module Tree
To give Rust the path to module members we need there’s two ways.
- An absolute path is the full path starting from a crate root.
- For code from an external crate, the absolute path begins with the crate name, and for code from the current crate, it starts with the literal
crate.
- For code from an external crate, the absolute path begins with the crate name, and for code from the current crate, it starts with the literal
- A relative path starts from the current module and uses
self,super, or an identifier in the current module. Both ways paths are followed by one or more identifiers separated by double colons by::.
This portion of the chapter is dense but contains some best practices and how the Rust community does things, I’ll simply reference it here.
Public Structs and Enums
Using pub only makes a struct or enum public, not its fields. To make them public we can choose which to expose, also using the pub keyword.
The same goes for associated functions define inside an enum or struct.
“Enums aren’t very useful unless their variants are public; it would be annoying to have to annotate all enum variants with
pubin every case, so the default for enum variants is to be public.”
The use keyword and Scopes
As mentioned before, instead of writing the paths over and over again we can bring them into scope with the use keyword. // often at the top of a file/module.
It’s similar to making a symbolic link in a file system.
Idiomatic use
The convention is to bring in the parent of the item you want to use.
Then using the :: operator to tap into it when you need it, this reduces the amount of use statements for each child of a module…
For example:
use crate::front_of_house::hosting and then calling hosting::add_to_waitlist in eat_at_restaurant.
However, with structs, enums, and other items, the convention is to specify the full path.
E.g. use std::collections::HashMap; instead of use std::collections;
// In think this is to prevent loading items you have no use for, but have no proof...
Aliases with as keyword
This is great for when you need to import items with the same name but from different paths or modules. By assigning aliases you avoid confusing the two and leading to all sorts of havoc. For example:
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}Re-Exports using pub use
Names brought into scope are private. However, sometimes you may need to refer to that name outside, and that’s where pub + use come in.
This is known as Re-Exporting.
Example:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}Re-exporting is handy when the internal structure of our project is different from how programmers calling and using the project’s code would think of the domain,thus improving the library’s organization.
The Glob Operator
To bring ALL the items in a path into scope, specify it with use and then use * operator at the end of the path.
use std::collections::*;This will bring everything into scope from the Collections module.
However this makes it hard to tell what’s actually in scope and where a name is defined in the program, and can cause issues if things are moved around…
The Glob operator is used in testing modules to bring everything in scope to be tested.
Breaking Down Modules into Different Files
Although you can define modules in a single file, and there are cases when that’s best. Most times you’re likely better off to separate your modules into their own files. This makes it easier to navigate the code base.
In the module’s file declare it as a module using pub mod <name>; and expose items using pub as appropriate.
Summary
Alternate File Paths
So Rust supports two styles of file paths for imports.
For a module named front_of_house declared in the crate root, the compiler will look for the module’s code in:
src/front_of_house.rsthe current style.src/front_of_house/mod.rsolder style that’s still supported.
For a module named hosting that is a submodule of front_of_house, the compiler will look for the module’s code in:
src/front_of_house/hosting.rsthe current style.src/front_of_house/hosting/mod.rsolder style that’s still supported.
Don't use both styles for the same module, you’ll get a compiler error.
Using a mix of both styles for different modules in the same project is allowed, but might confuse other developers using your project…
I don’t like the the style that uses files named mod.rs because your project becomes cluttered with many mod.rs files.
Summary
- We can break down our code into modules, and organize them into separate files.
- Packages are made of crates, some are binary crates and others are library crates.
- A Package must be at least one crate, and may only have one library crate at the time.
- We can bring code into scope with the
usekeyword. - Module code is private by default unless made public using
pubkeyword, and their members must also be made public to be used.