Ch 7. Managing Growing Projects with Packages, Crates, and Modules
7.1 Packages and Crates
Crates
-
A crate is the smallest unit of code that the Rust compiler works with at a time.
-
Crates can be:
- Binary Crates: Programs compiled to executables (e.g., command-line tools, servers). They must have a
mainfunction. - Library Crates: Code intended to be shared, with no
mainfunction. Examples include libraries likerandfor generating random numbers.
- Binary Crates: Programs compiled to executables (e.g., command-line tools, servers). They must have a
-
The crate root is the source file where the Rust compiler starts compiling your crate. For binary crates, it’s typically
src/main.rs. For library crates, it’ssrc/lib.rs.
Packages
-
A package is a bundle of one or more crates, typically including at least one crate.
-
A package contains:
- A
Cargo.tomlfile that defines how to build the crate(s). - The source code for the crate(s) inside the
srcdirectory.
- A
-
A package can contain:
- One library crate (
src/lib.rs). - Multiple binary crates, each in separate files under
src/bin/.
- One library crate (
Example: Creating a New Package
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rsCargo.toml: Defines the package configuration (dependencies, version, etc.).src/main.rs: The crate root for the binary cratemy-project.
Key Conventions in Cargo
- If
src/main.rsis present, the package contains a binary crate. - If
src/lib.rsis present, the package contains a library crate. - A package can contain both
src/main.rs(binary) andsrc/lib.rs(library) crates with the same name as the package. - Additional binary crates can be placed in the
src/bin/directory, where each file represents a separate binary crate.
Best Practices for Packages with Both a Binary and a Library Crate
When a package contains both a binary crate and a library crate, the typical goal is to maximize code reuse by organizing most of the logic in the library, while the binary crate acts primarily as an entry point to run the program.
-
Separation of Concerns:
- The binary crate (defined in
src/main.rs) should focus on application-specific tasks like handling command-line arguments, configuration, and invoking the library crate’s functionality. - The library crate (defined in
src/lib.rs) should contain the core functionality that can be reused by other projects or other parts of the same project.
- The binary crate (defined in
-
Library-Centric Design:
- The module tree should be defined in
src/lib.rs. All the core functionality, such as helper functions, data processing, and business logic, should reside in the library crate. - The binary crate (
src/main.rs) should act as a thin wrapper that starts the executable and interacts with the public API of the library.
- The module tree should be defined in
-
Public API:
- The library crate should expose its functionality via public items (functions, structs, enums, etc.) to be reusable both within the package and by external users.
- The binary crate interacts with the library crate through its public API, using paths that start with the package name, similar to how external crates access it.
Benefits of This Organization
-
Code Reuse:
- By placing most of the functionality in the library crate (
src/lib.rs), you allow other projects and even other parts of the same package to reuse the code. - The binary crate (
src/main.rs) simply starts the application and calls into the library crate, keeping the binary code minimal.
- By placing most of the functionality in the library crate (
-
Testing and Maintenance:
- The library crate can be tested independently of the binary crate, making it easier to write unit tests for the core logic.
- If changes are needed in the functionality, you can update the library crate without having to modify the binary entry point, unless changes in behavior are required at the application level.
-
API Design:
- The binary crate is a consumer of the public API of the library crate, which helps ensure the library’s API is well-designed and user-friendly. As the package author, you also act as a user of your own API.
- This practice encourages encapsulation and modularity, as the binary crate can only access the library crate’s public items.
7.2 Modules in Rust: Controlling Scope and Privacy
Rust’s module system allows you to organize your code into smaller, manageable parts, control the visibility (privacy) of your items, and handle code dependencies efficiently.
Cheat Sheet for Modules, Paths, and Visibility
-
Crate Root:
- The compiler starts at the crate root file, which is
src/lib.rsfor library crates andsrc/main.rsfor binary crates.
- The compiler starts at the crate root file, which is
-
Declaring Modules:
- To declare a module, use the
modkeyword. For example,mod garden;in the crate root tells Rust to look for thegardenmodule in:- Inline (using curly brackets after
mod garden) src/garden.rssrc/garden/mod.rs
- Inline (using curly brackets after
- To declare a module, use the
-
Declaring Submodules:
- In any module, you can declare submodules. For example,
mod vegetables;insidesrc/garden.rswill look for the submodule in:- Inline (with curly brackets after
mod vegetables) src/garden/vegetables.rssrc/garden/vegetables/mod.rs
- Inline (with curly brackets after
- In any module, you can declare submodules. For example,
-
Paths to Items in Modules:
- Once a module is part of your crate, you can access items in that module using the full path. For example, an
Asparagustype in thegarden::vegetablesmodule can be accessed viacrate::garden::vegetables::Asparagus.
- Once a module is part of your crate, you can access items in that module using the full path. For example, an
-
Private vs. Public:
- Private by Default: Items in a module are private to their parent module by default.
- Making Modules Public: To make a module public, use
pub mod. - Making Items Public: Use
pubbefore items like functions, structs, or enums to make them accessible outside their module.
-
The
useKeyword:- The
usekeyword creates shortcuts for long paths. For example,use crate::garden::vegetables::Asparagus;allows you to refer toAsparagusdirectly without the full path in the current scope.
- The
Example: Organizing a Crate
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rssrc/main.rs (Crate Root for Binary Crate)
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}use crate::garden::vegetables::Asparagus;: Brings theAsparagustype into scope, so you can refer to it directly.pub mod garden;: Declares thegardenmodule, telling the compiler to load code fromsrc/garden.rs.let plant = Asparagus {};: Creates an instance ofAsparagus, which is defined in thevegetablessubmodule.
src/garden.rs (Module File for garden)
pub mod vegetables;pub mod vegetables;: Declares thevegetablessubmodule, making it public and telling the compiler to load code fromsrc/garden/vegetables.rs.
src/garden/vegetables.rs (Submodule File for vegetables)
#[derive(Debug)]
pub struct Asparagus {}#[derive(Debug)]: Allows theAsparagusstruct to be printed withprintln!using the{:?}formatter.pub struct Asparagus {}: Defines a publicAsparagusstruct so that it can be used outside thevegetablesmodule.
Module Rules in Action
-
Start from the crate root: The compiler starts with
src/main.rs(for a binary crate), and thepub mod garden;tells it to load thegardenmodule fromsrc/garden.rs. -
Declare modules and submodules: The
gardenmodule is declared insrc/main.rs, and thevegetablessubmodule is declared insrc/garden.rs. -
Use paths: Inside
main.rs, we can access theAsparagustype via the full pathcrate::garden::vegetables::Asparagusor use theusekeyword for a shortcut. -
Control privacy: The
pubkeyword makes both thegardenandvegetablesmodules public, and theAsparagusstruct is also public so it can be accessed frommain.rs.
The use Keyword in Detail
The use keyword simplifies code by allowing shorter references. Instead of writing the full path every time, you can import items into scope. For example:
use crate::garden::vegetables::Asparagus;
fn main() {
let plant = Asparagus {};
}Without use, you would need to write the full path each time:
fn main() {
let plant = crate::garden::vegetables::Asparagus {};
}Grouping Related Code in Modules
- Modules in Rust allow you to organize your code into logical groups, making it easier to manage and control the visibility of functions, structs, and other items.
- By default, items within a module are private, but they can be made public using the
pubkeyword.
Step 1: Creating the Project
-
Run the following command to create a new library project:
cargo new restaurant --lib -
This creates a
restaurantdirectory with aCargo.tomlfile and asrc/lib.rsfile.
Step 2: Defining Modules
In src/lib.rs, we can group related functionalities into nested modules to reflect how a restaurant operates.
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() {}
}
}mod front_of_house: This defines a module namedfront_of_house.mod hostingandmod serving: These are submodules nested withinfront_of_house, each containing related functions.
Step 3: The Module Tree
The structure created above forms a module tree
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment- Modules and nesting:
hostingandservingare sibling modules, both contained within the parent modulefront_of_house.
Key Concepts
-
Module Definition: Use the
modkeyword followed by the module name:mod front_of_house { // nested modules and items } -
Nested Modules: You can nest modules within other modules. Each module can contain functions, structs, enums, traits, and other items.
mod hosting { fn add_to_waitlist() {} } -
Private by Default: Functions and items inside modules are private unless explicitly made public with the
pubkeyword.
Crate and Module Tree
Rust’s module system is like a filesystem directory tree, where:
- The crate root (
src/lib.rsorsrc/main.rs) forms the root of the module tree. - Nested modules are like subdirectories.
- The crate itself forms an implicit module that acts as the root for all other modules.
7.3 Paths for Referring to an Item in the Module Tree
- In Rust, paths are used to refer to items like functions, structs, and modules within the module tree. There are two types of paths:
- Absolute paths: Start from the root of the crate or an external crate.
- Relative paths: Start from the current module using
self,super, or identifiers within the current module.
// src/lib.rs
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();
}-
Absolute Path:
- The first call to
add_to_waitlistuses the full path starting from the crate root withcrate::front_of_house::hosting::add_to_waitlist().
- The first call to
-
Relative Path:
- The second call uses a relative path, assuming we’re in the same module as
front_of_houseandhosting.
- The second call uses a relative path, assuming we’re in the same module as
Privacy and Module Access
-
By default, everything in a module is private, meaning it’s only accessible within the module itself.
-
To make items accessible to other parts of the crate, we need to use the
pubkeyword to make them public.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported// src/lib.rs, fixed with pub
mod front_of_house {
pub mod hosting {
pub 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();
}pub mod hosting {}: Makes thehostingmodule public so it can be accessed from outsidefront_of_house.pub fn add_to_waitlist(): Makes theadd_to_waitlistfunction public so it can be called outside thehostingmodule.
Now, both absolute and relative paths will work because the items are public.
Module Tree with Privacy
crate
└── front_of_house (private)
└── hosting (public)
└── add_to_waitlist (public)In this tree:
front_of_houseis private, but its child modulehostingis public.add_to_waitlistis a public function within thehostingmodule.
Absolute vs. Relative Paths
Choosing whether to use an absolute or relative path depends on your project structure:
-
Absolute Paths:
- More stable if you plan to move code around. For example, moving the
eat_at_restaurantfunction to another module wouldn’t require updating the absolute path.
- More stable if you plan to move code around. For example, moving the
-
Relative Paths:
- Handy when modules are closely related and likely to stay together. If the
eat_at_restaurantfunction and thefront_of_housemodule are moved together, the relative path remains valid.
- Handy when modules are closely related and likely to stay together. If the
Exposing Paths with the pub Keyword
Marking a module as pub only makes the module itself accessible but does not automatically expose its contents. You also need to mark individual items (like functions or structs) within the module as pub to make them accessible.
mod front_of_house {
pub mod hosting {
pub 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();
}pub mod hosting {}: This makes thehostingmodule accessible from outside thefront_of_housemodule.pub fn add_to_waitlist() {}: This makes theadd_to_waitlistfunction accessible outside thehostingmodule.
Explanation of Privacy and Paths
-
Absolute Path:
- The call
crate::front_of_house::hosting::add_to_waitlist()works because:- The
front_of_housemodule is private, buteat_at_restaurantis in the same module asfront_of_house, so it can access it. - The
hostingmodule is public (pub), so it can be accessed fromeat_at_restaurant. - The
add_to_waitlistfunction is public (pub), so it can be called from outside thehostingmodule.
- The
- The call
-
Relative Path:
- The call
front_of_house::hosting::add_to_waitlist()works because:front_of_houseis defined in the same module aseat_at_restaurant, so the relative path works.- The
hostingmodule and theadd_to_waitlistfunction are both public, so they can be accessed.
- The call
Using super for Relative Paths
- In Rust, you can use the
superkeyword to create relative paths that start from the parent module, just like how..is used in filesystem paths. - This can be useful when you need to reference items in a parent module without starting from the crate root, making it easier to restructure your module tree if necessary.
Example: Using super for Relative Paths (Listing 7-8)
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order(); // Call a function from the parent module
}
fn cook_order() {}
}super::deliver_order(): This relative path starts in the parent module (crate) and looks for thedeliver_orderfunction. The use ofsupermakes it easier to move modules around while maintaining correct references.- In this case, the
back_of_housemodule referencesdeliver_order, which resides in the parent module (the crate root).
Making Structs and Enums Public
In Rust, the pub keyword is used to make structs, enums, and their fields/variants public, but there are specific rules for how visibility works for structs and enums.
Making Structs Public
When you make a struct public using pub, its fields remain private by default. You must explicitly mark each field as public if you want it to be accessible outside the module.
Example: Public Struct with Some Private Fields
mod back_of_house {
pub struct Breakfast {
pub toast: String, // Public field
seasonal_fruit: String, // Private field
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"), // Private field set internally
}
}
}
}
pub fn eat_at_restaurant() {
let mut meal = back_of_house::Breakfast::summer("Rye");
meal.toast = String::from("Wheat"); // Public field can be modified
println!("I'd like {} toast, please", meal.toast);
// Error: meal.seasonal_fruit is private and can't be accessed
// meal.seasonal_fruit = String::from("blueberries");
}- Public Field (
toast): Thetoastfield is marked aspub, so it can be accessed and modified outside theback_of_housemodule. - Private Field (
seasonal_fruit): Theseasonal_fruitfield is private, so it can only be accessed within the module or through functions within the module. It cannot be modified ineat_at_restaurant. - Constructor Function (
summer): Sinceseasonal_fruitis private, theBreakfaststruct provides a public constructor method (summer) to initialize instances.
Making Enums Public
When an enum is made public, all of its variants automatically become public. There’s no need to mark individual variants as public, unlike struct fields.
Example: Public Enum
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}- Public Enum (
Appetizer): Making theAppetizerenum public automatically makes its variants (SoupandSalad) accessible outside the module. - Variants Public by Default: Enum variants are public when the enum is public, which differs from struct fields that are private by default unless explicitly made public.
7.4 Bringing Paths into Scope with the use Keyword
In Rust, writing out full paths to access functions or modules can be repetitive. To make code more concise, the use keyword allows you to bring a path into scope, creating a shortcut that can be used throughout that scope.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist(); // Shortcut to hosting::add_to_waitlist
}use crate::front_of_house::hosting;: This brings thehostingmodule into scope, so we don’t need to write the full pathcrate::front_of_house::hostingevery time we use it.- This is similar to creating a symbolic link in a filesystem, where you create an alias for a longer path.
Scope of use
The use keyword only applies within the scope in which it is declared. If you declare use at the crate root, it doesn’t affect submodules. Each module has its own scope, so you need to repeat the use statement in other modules if you want to use the shortcut there.
If you try to use the hosting shortcut in a different module, such as a new customer module, the code will not compile:
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist(); // Error: hosting is not in this scope
}
}The compiler will produce an error because the use crate::front_of_house::hosting; statement only applies to the scope where it’s declared (in this case, the crate root), not in the customer module:
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`To resolve this issue, you have a few options:
-
Move the
useStatement into thecustomerModule:You can place the
usestatement inside thecustomermodule so that it applies within that module’s scope:mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } mod customer { use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); // Now works because hosting is in scope } } -
Use
superto Access the Parent Module:If the
hostingmodule is in the parent module, you can usesuper::to access it from within thecustomermodule:mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} } } mod customer { pub fn eat_at_restaurant() { super::super::front_of_house::hosting::add_to_waitlist(); // Using super to refer to parent } }
Compiler Warning for Unused Imports
- you might also get a warning if the
usestatement is declared but not used within its scope:
warning: unused import: `crate::front_of_house::hosting`This occurs because the use statement is declared in the crate root, but it’s not actually used there. This can be fixed by moving the use statement into the appropriate scope where it is needed, such as inside the customer module.
Idiomatic use Paths
Rust provides idiomatic patterns for using the use keyword to bring items into scope. This not only improves code readability but also follows conventions that are familiar to the Rust community.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting; // Bring the module into scope
pub fn eat_at_restaurant() {
hosting::add_to_waitlist(); // Call the function through the module
}This is idiomatic because:
- By bringing the module (
hosting) into scope, the code still indicates thatadd_to_waitlistis not locally defined—it’s part of thehostingmodule. - This improves code clarity and avoids confusion about the function’s origin.
Unidiomatic Example (Listing 7-13)
use crate::front_of_house::hosting::add_to_waitlist; // Bring function directly into scope
pub fn eat_at_restaurant() {
add_to_waitlist(); // Less clear where the function is defined
}While this approach works, it is unidiomatic because:
- It makes the function look like it is locally defined, hiding its module origin.
- The code becomes less clear about the function’s source, which can lead to confusion when reading or maintaining the code.
Bringing Structs, Enums, and Other Items into Scope: Full Paths
When bringing structs or enums into scope, it is idiomatic to specify the full path
use std::collections::HashMap; // Bring the struct directly into scope
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}- This pattern is commonly used with structs like
HashMap, and it is a familiar convention in Rust. - It provides a clear indication of the origin of the type.
Handling Naming Conflicts with the as Keyword
When two items with the same name are brought into scope, Rust will throw an error due to ambiguity. You can resolve this by using their parent modules or by using the as keyword to provide an alias.
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
}
fn function2() -> io::Result<()> {
// --snip--
}By keeping fmt::Result and io::Result within their parent modules, we avoid conflicts between the two types that share the same name (Result).
You can also rename one of the types using as:
use std::fmt::Result;
use std::io::Result as IoResult; // Alias to avoid conflict
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}- The
askeyword creates an alias (IoResult), allowing bothResulttypes to coexist without conflict. - This approach is also idiomatic and offers flexibility when working with naming conflicts.
Re-exporting Names with pub use
Sometimes, you may want to re-export items from your module to simplify external code that depends on your library. You can do this using the pub use combination, which exposes internal items as part of your public API.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting; // Re-export the hosting module
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}pub useallows external code to refer tohostingdirectly viarestaurant::hosting::add_to_waitlist(), instead of the full pathrestaurant::front_of_house::hosting::add_to_waitlist().- This makes your public API simpler and easier to use while allowing you to maintain a clean internal structure.
When to Use pub use
Re-exporting is useful in cases where:
- You want to hide internal complexity or module hierarchy from external users.
- You want to provide a cleaner, more intuitive public API that aligns with how external users will think about your code.
For example, in the context of a restaurant, internal staff might think of “front of house” and “back of house,” but customers just care about the services available (e.g., hosting). Re-exporting allows you to keep internal code organized while exposing a cleaner interface.
Using External Packages in Rust
In Rust, external packages (or “crates”) are stored and shared via crates.io , the Rust package registry. To use an external package in your project, you follow these steps:
1. Add the Dependency to Cargo.toml
First, you need to specify the external package and its version in your project’s Cargo.toml file. For example, to use the rand crate, you would add:
[dependencies]
rand = "0.8.5"- This tells Cargo to download the specified version of the
randcrate from crates.io , along with any of its dependencies.
2. Bring the External Package into Scope with use
Once the dependency is listed in Cargo.toml, you can bring it into scope in your code by using the use keyword, just like you do with standard library items.
For example, in the guessing game example from Chapter 2, we used the rand crate to generate random numbers:
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}rand::Rng: This brings theRngtrait from therandcrate into scope, enabling us to call methods likegen_range.rand::thread_rng(): This function provides a random number generator that’s local to the current thread.
3. Accessing the Standard Library Crate
The standard library (std) is also an external crate, but it’s included with Rust, so you don’t need to add it to Cargo.toml. However, you still need to bring its components into scope using use, similar to how you handle external crates.
For example, to use a HashMap from the standard library, you would write:
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}std::collections::HashMap: This imports theHashMaptype from thestdcrate’scollectionsmodule.
Using Nested Paths to Clean Up Large use Lists
When multiple items from the same module or crate need to be brought into scope, Rust allows you to use nested paths to group these imports into a single use statement.
use std::{cmp::Ordering, io};std::{cmp::Ordering, io}: This statement brings bothstd::cmp::Orderingandstd::iointo scope in a single line.- This approach helps reduce the vertical space taken up by multiple
usestatements.
Nested Paths at Any Level
You can also use nested paths at any level of a path, which is helpful when dealing with multiple imports that share a common subpath.
use std::io::{self, Write};self: Refers to the current module (std::io), so this imports bothstd::ioandstd::io::Writein one line.
The Glob Operator in Rust (*)
The glob operator (*) allows you to bring all public items from a module or crate into scope in one statement, without specifying each item individually.
use std::collections::*;- This statement brings all public items from the
std::collectionsmodule into scope, such asHashMap,VecDeque,BTreeMap, and others. - You don’t need to specify each item manually, which can be convenient in certain situations.
When to Use the Glob Operator
The glob operator can simplify imports, but it should be used with caution because it can make it harder to know where particular items come from, leading to less readable code. Here are the most common scenarios where it might be used:
-
In Tests:
- When writing tests, the glob operator is often used to bring all items from the
testmodule into scope. - For example, importing everything from a
testsmodule when you want access to all the test functions and utilities.
- When writing tests, the glob operator is often used to bring all items from the
-
Prelude Pattern:
- The prelude pattern in Rust allows certain frequently-used items to be automatically brought into scope for convenience. The glob operator is used internally to make these items readily available without requiring users to explicitly import them.
- The standard library prelude includes items like
Option,Result, andVec, which are automatically in scope in every Rust program.
Potential Downsides of Using the Glob Operator
-
Ambiguity:
- Using
*can make it harder to know exactly which items are being brought into scope, especially in larger programs or when multiple modules are involved. - This can lead to confusion or naming conflicts if two items with the same name are imported from different modules.
- Using
-
Decreased Readability:
- When you use the glob operator, readers of the code may not immediately know where a specific item (e.g., a struct or function) is coming from, which can reduce the clarity and maintainability of the code.
-
Naming Conflicts:
- If multiple modules contain items with the same name, using the glob operator may result in naming conflicts that are difficult to resolve.
7.5 Separating Modules into Different Files in Rust
As your Rust project grows, you may want to organize your code by splitting modules into separate files. Rust makes this easy and idiomatic by associating modules with specific file structures.
Step 1: Moving the front_of_house Module to a Separate File
First, let’s take the front_of_house module and move it into its own file.
1.1 Modify src/lib.rs
Remove the body of the front_of_house module and replace it with a mod declaration
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}- The
mod front_of_house;tells Rust to look for thefront_of_housemodule definition in an external file (in this case,src/front_of_house.rs). - This code won’t compile until you create the
src/front_of_house.rsfile.
1.2 Create src/front_of_house.rs
Now, move the contents of the front_of_house module into a new file named src/front_of_house.rs:
pub mod hosting {
pub fn add_to_waitlist() {}
}- This file defines the
front_of_housemodule and its submodulehosting.
Step 2: Moving the hosting Submodule to a Separate File
Now, let’s move the hosting module into its own file.
2.1 Modify src/front_of_house.rs
Replace the body of the hosting module with a mod declaration:
pub mod hosting;- This tells Rust to look for the
hostingmodule in a file namedsrc/front_of_house/hosting.rs.
2.2 Create src/front_of_house/hosting.rs
Now, move the contents of the hosting module into a new file named src/front_of_house/hosting.rs:
pub fn add_to_waitlist() {}- This file now contains the implementation of the
hostingmodule.
How Rust Finds Module Files
-
For modules declared at the crate root (
lib.rsormain.rs), Rust looks for files in either:src/module_name.rs(recommended and more modern)src/module_name/mod.rs(older style, still supported)
-
For submodules inside other modules (e.g.,
hostinginsidefront_of_house), Rust expects files in:src/parent_module/submodule.rs(recommended)src/parent_module/submodule/mod.rs(older style)
Note: Avoid mixing both styles for the same module (e.g., having both
src/module.rsandsrc/module/mod.rs), as this will result in a compiler error.
Why Split Modules into Separate Files?
-
Improved Readability: As your project grows, having all modules in a single file can make it difficult to navigate. Splitting code into multiple files improves readability.
-
Logical Separation: Large modules can be broken into smaller, logical pieces, allowing developers to focus on a specific part of the code.
-
Maintainability: With well-organized code spread across multiple files, it becomes easier to manage, debug, and extend the project.
Alternate File Paths
-
Modern Style (Recommended):
src/module_name.rsfor top-level modules.src/parent_module/submodule.rsfor submodules.
-
Older Style (Still Supported):
src/module_name/mod.rsfor top-level modules.src/parent_module/submodule/mod.rsfor submodules.
The modern style avoids multiple files named mod.rs, which can be confusing when multiple such files are open in your editor.