Modules in Rust

braindead
Dev Genius
Published in
8 min readDec 13, 2022

--

Organising your code will become increasingly important as you write larger programmes. By grouping related functionality and separating code with distinct features, you can make it clear where to look for code that implements a specific feature and where to go to change how a feature works.

So far, all of our programmes have been contained within a single module and a single file. As a project grows, you should organise the code by breaking it down into modules and then files. A package may contain multiple binary crates as well as one or more library crates. As a package grows, parts can be separated into separate crates, which become external dependencies. All of these techniques are covered in this chapter. Cargo provides workspaces for very large projects that consist of a collection of interconnected packages that evolve together.

We’ll also talk about encapsulating implementation details, which allows you to reuse code at a higher level by allowing other code to call your code via its public interface without knowing how the implementation works. The manner in which you write code determines which parts are public for use by other code and which are private implementation details that you reserve the right to change. This is another method for limiting the amount of information you must remember.

Scope is a related concept: the nested context in which code is written has a set of names defined as “in scope.” When reading, writing, and compiling code, programmers and compilers must understand whether a specific name at a specific location refers to a variable, function, struct, enum, module, constant, or other item, and what that item means. You can define scopes and change which names are included or excluded from them. Two items with the same name cannot exist in the same scope; tools are available to resolve name conflicts.

Rust provides a number of features for managing the organisation of your code, such as which details are exposed, which details are private, and which names are in each scope in your programs. These features, which are sometimes referred to collectively as the module system, include:

Packages are a Cargo feature that allows you to create, test, and share crates.

Crates: A collection of modules that results in a library or executable.

Modules and application: Allow you to control the organisation, scope, and privacy of paths.

Paths: A method of naming an object, such as a struct, function, or module.

Crates and packages

The first module system components we’ll look at are packages and crates.

A crate is the smallest amount of code considered by the Rust compiler at any given time. Even if you use rustc instead of cargo and pass a single source code file (as we did in Chapter 1’s “Writing and Running a Rust Program” section), the compiler considers that file to be a crate. Modules can be included in crates, and the modules can be defined in other files that are compiled alongside the crate, as we’ll see in the following sections.

A crate is classified as either a binary crate or a library crate. Binary crates are programmes that can be compiled into executables, such as a command-line programme or a server. Each must include a main function that defines what happens when the executable is executed. So far, all of the crates we’ve built have been binary crates.

Library crates lack a main function and do not compile to an executable. Instead, they define functionality that will be shared by several projects.

When Rustaceans say “crate,” they mean library crate, and they interchange “crate” with the general programming concept of a “library.”

The crate root is a source file from which the Rust compiler generates the root module of your crate.

A package is a collection of one or more crates that provide a specific set of functionality. A Cargo.toml file in a package describes how to build those crates. Cargo is a package that contains the binary crate for the command-line tool you used to build your code. The Cargo package also includes a library crate on which the binary crate is dependent. Other projects can rely on the Cargo library crate to implement the logic used by the Cargo command-line tool.

A package can have as many binary crates as it wants, but only one library crate. A package must include at least one crate, which can be either a library or a binary crate.

Let’s go over what happens when we make a package. First, we add a new command cargo:

We use ls to see what Cargo creates after we run cargo new. A Cargo.toml file in the project directory provides us with a package. There’s also a src directory with main.rs in it. When you open Cargo.toml in your text editor, you’ll notice that there’s no mention of src/main.rs. Cargo follows the convention that the crate root of a binary crate with the same name as the package is src/main.rs. Similarly, if the package directory contains src/lib.rs, Cargo knows that the package contains a library crate with the same name as the package, and src/lib.rs is its crate root. Cargo sends the crate root files to rustc, which generates the library or binary.

We have a package that only has src/main.rs, which means it only has a binary crate named my-project. If a package includes src/main.rs and src/lib.rs, it contains two crates: a binary and a library with the same name as the package. By placing files in the src/bin directory, a package can have multiple binary crates: each file is a separate binary crate.

Cheat Sheet for Modules

Here’s a quick guide to how modules, paths, the use keyword, and the pub keyword work in the compiler, as well as how most developers organise their code.

When compiling a crate, the compiler looks for code to compile in the crate root file (usually src/lib.rs for a library crate or src/main.rs for a binary crate).

Module declaration: You can declare new modules in the crate root file; for example, mod garden; declares a “garden” module. The compiler will look for module code in the following locations:

Within curly brackets that replace the semicolon after mod garden

In the src/garden.rs file

In the directory src/garden/mod.rs

Declaring submodules: Submodules can be declared in any file other than the crate root. In src/garden.rs, for example, you could declare mod vegetables. The compiler will look for the submodule’s code in the parent module’s directory in the following locations:

Instead of a semicolon, use curly brackets inline, directly following mod vegetables.

In the src/garden/vegetables.rs file

In the directory src/garden/vegetables/mod.rs

Paths to code in modules: Once a module is in your crate, you can use the path to the code to refer to code in that module from anywhere else in the same crate, as long as the privacy rules allow. In the garden vegetables module, for example, an Asparagus type can be found at crate::garden::vegetables::Asparagus.

Private vs public: By default, code within a module is kept private from its parent modules. To make a module public, use pub mod instead of mod. Use pub before the declarations of items within a public module to make them public as well.

The use keyword: The use keyword creates shortcuts to items within a scope to reduce the repetition of long paths. You can create a shortcut using crate::garden::vegetables::Asparagus in any scope that can refer to crate::garden::vegetables::Asparagus, and then you only need to write Asparagus to use that type in the scope.

We’ll make a binary crate called backyard to demonstrate these rules. The following files and directories are found in the crate’s directory, also known as backyard:

In this case, the crate root file is src/main.rs, which contains:

src/main.rs is the filename.

Rust

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {

let plant = Asparagus {};

println!(“I’m growing {:?}!”, plant);

}

The pub mod garden; line instructs the compiler to include the code found in src/garden.rs, which is as follows:

src/garden.rs is the filename.

Rust

pub mod vegetables;

In this case, pub mod vegetables; refers to the code in src/garden/vegetables. rs is also included. This is the code:

Rust

#[derive(Debug)]

pub struct Asparagus {}

Let’s get into the specifics of these rules and see how they work!

Modules for Grouping Related Code

Modules allow us to organise code within a crate for easier readability and reuse. Because code within a module is private by default, modules allow us to control the privacy of items. Private items are internal implementation details that are not accessible to the public. We can make modules and the items contained within them public, which exposes them and allows external code to use and rely on them.

As an example, consider creating a library crate that performs the functions of a restaurant. We’ll define function signatures but leave their bodies empty in order to focus on code organisation rather than restaurant implementation.

Some parts of a restaurant are referred to as front of house and others as back of house in the restaurant industry. Front of house refers to the area where customers are seated, servers take orders and payment, and bartenders make drinks. Back of house is where chefs and cooks work in the kitchen, dishwashers clean up after themselves, and managers do administrative work.

We can organise our crate’s functions into nested modules to structure it in this manner. Run cargo new restaurant — lib to create a new library named restaurant, and then copy the code from Listing 7–1 into src/lib.rs to define some modules and function signatures. The following is the front of house section:

src/lib.rs is the filename.

Rust

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() {}

}

}

A front of house module that contains other modules, each of which contains functions.

The mod keyword is followed by the module name (in this case, front of house) to define a module. The module’s body is then enclosed in curly brackets. Other modules can be placed within modules, as in this case with the hosting and serving modules. Modules can also store definitions for other types of objects, such as structs, enums, constants, traits, and functions, as shown in Listing 7–1.

We can group related definitions together and name why they’re related by using modules. Programmers can navigate the code based on the groups rather than having to read through all of the definitions, making it easier to find the definitions that are relevant to them. Programmers who add new functionality to this code will know where to put it in order to keep the programme organised.

We already mentioned that src/main.rs and src/lib.rs are crate roots. The contents of either of these two files form a module called crate at the root of the crate’s module structure, known as the module tree.

The module tree for the code in Listing

This tree depicts how some modules nest inside one another, such as hosting nests inside front of house. The tree also reveals that some modules are siblings, which means they are defined in the same module; for example, hosting and serving are siblings defined within front of house. If module A is contained within module B, we say that module A is its child and that module B is its parent. It’s worth noting that the entire module tree is rooted in the implicit module crate.

The module tree may remind you of your computer’s filesystem’s directory tree; this is an excellent analogy! Modules, like directories in a filesystem, are used to organise your code. And, like files in a directory, we must be able to locate our modules.

--

--

A passionate programmer, I am eager to challenge myself to do things I’ve never accomplished before and I strive to learn and improve on my skills every day