17.3 Modules: Organizing Code Within Crates

Modules are used to encapsulate Rust source code, hiding internal implementation details. Only items marked with the pub keyword are accessible from outside the module.

17.3.1 What Is a Module and Its Purpose?

A module is a namespace that contains definitions of functions, structs, enums, constants, traits, and other modules. Modules serve several purposes:

  • Encapsulation: Hide implementation details and expose only necessary parts of the code.
  • Organization: Group related functionality together.
  • Namespace Management: Prevent naming conflicts by providing separate scopes.

From outside of a module, only items explicitly exported using the pub keyword are visible. To access public items, you must prefix item names with the module names separated by ::. For deeply nested modules, these prefixes, which are sometimes referred to as paths, can become quite long, like std::collections::HashMap. The use keyword allows us to shorten these paths for items, as long as no name conflicts occur.

17.3.2 Module Syntax and File-Based Organization

Modules can be defined inline or in separate files.

Inline Modules

Inline modules can be used to group Rust code and create a separate namespace. To create an inline module in a source code file, we start the code block with the mod keyword and the name of the module. The code inside the module is then invisible from outside, except for items marked with the pub keyword, which can be accessed by prefixing the item name with the module name:

Example:

mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}
fn main() {
    let sum = math::add(5, 3);
    println!("Sum: {}", sum);
}

Note that the math module itself is visible from the main function, so it is not necessary to mark the math module with the pub keyword like pub mod math. This is a general Rust design—module names declared on the same level are always visible and are sometimes called sibling modules. But items inside the math module have to be marked with pub to be visible from outside. If the math module has a submodule, that one would need the pub keyword to become visible from outside of the parent (math) module.

File-Based Modules

Larger Rust modules are typically stored in separate files. These files contain ordinary Rust code and are stored in the src folder. To use the public items of these modules from other Rust code, these modules have to be imported with the mod keyword:

Example Structure:

my_crate/
├── src/
│   ├── main.rs
│   └── math.rs

src/math.rs:

#![allow(unused)]
fn main() {
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
}

src/main.rs:

mod math;
fn main() {
    let sum = math::add(5, 3);
    println!("Sum: {}", sum);
}

Submodules

Modules can contain submodules, which can also be inline or in files.

Inline Submodules
mod math {
    pub mod operations {
        pub fn add(a: i32, b: i32) -> i32 {
            a + b
        }
    }
}
fn main() {
    let sum = math::operations::add(5, 3);
    println!("Sum: {}", sum);
}

Note that the module math needs no pub prefix, as it is a top-level module with the same level as the main() function which accesses it. However, the submodule operations as well as the function add() are both enclosed in an outer module (math) and require the pub prefix to become publicly visible.

File-Based Submodules

File-based submodules behave very similarly to inline ones.

Example Structure:

my_crate/
├── src/
│   ├── main.rs
│   ├── math.rs
│   └── math/
│       └── operations.rs

src/main.rs:

mod math;
fn main() {
    let product = math::operations::multiply(5, 3);
    println!("Product: {}", product);
}

src/math.rs:

pub mod operations; // Export this submodule

// Optional more code

src/math/operations.rs:

pub fn multiply(a: i32, b: i32) -> i32 {
    a * b
}

An important fact is that the mod keyword operates only on simple names, but never on paths. A statement like mod math::operations is invalid. If it were valid, importing submodules without importing their parent would be allowed, which generally is not intended and would be different from the behavior of inline modules. For this reason, the parent (math) of the submodule (operations) has to contain the statement pub mod operations; to export the submodule and make it accessible to the whole crate.

17.3.3 Alternate File Tree Layout

In this chapter, we used Rust's modern folder structure for file-based modules. However, an older structure, where module files like my_mod.rs are replaced by my_mod/mod.rs, is still supported.

For a toplevel module named math, the compiler will look for the module's code in:

  • src/math.rs (modern style)
  • src/math/mod.rs (older style)

For a module named operations that is a submodule of math, the compiler will look for the module's code in:

  • src/math/operations.rs (what we covered)
  • src/math/operations/mod.rs (older style, still supported path)

Mixing these styles for the same module is not allowed.

The main downside to the style that uses files named mod.rs is that your project can end up with many files named mod.rs, which can get confusing when you have them open in your editor at the same time.

17.3.4 Module Visibility and Privacy

By default, all items in a module are private to the parent module. You can control visibility using the pub keyword.

  • Private Items: Accessible only within the module and its child modules. When child modules have to access items of the parent module, the name prefix super:: has to be used.
  • Public Items (pub): Accessible from outside the module.

Example:

mod network {
    fn private_function() {
        println!("This is private.");
    }

    pub fn public_function() {
        println!("This is public.");
    }
}

fn main() {
    // network::private_function(); // Error: function is private
    network::public_function();      // OK
}

However, from inside a submodule, items defined in ancestor modules like functions and data types are always visible and can be used with paths like super::private_function().

Visibility of Structs and Enums

For enums, the visibility of variants is the same as the visibility of the enum itself. To make the whole enum with all its variants visible, we have to add a pub modifier only to the enum name itself.

Public Enum:

#![allow(unused)]
fn main() {
pub enum MyEnum {
    Variant1,
    Variant2,
}
}

For structs, the situation is different: Adding pub to the struct name makes only the struct type visible from outside of the module, but all fields remain hidden. For each field that should become visible as well, we have to add its own pub modifier. Creating instances of structs with hidden fields from outside of the module typically requires a constructor method, as we cannot assign values to hidden fields directly.

Public Struct with Private Fields:

#![allow(unused)]
fn main() {
pub struct MyStruct {
    pub public_field: i32,
    private_field: i32,
}

impl MyStruct {
    pub fn new() -> MyStruct {
        MyStruct {
            public_field: 0,
            private_field: 0,
        }
    }
}
}

17.3.5 Paths and Imports

To access items encapsulated in modules, you must prefix the item name with the module name or use special keywords like crate, self, or super. The prefix crate refers to the crate root, self refers to the current module, and super specifies the parent module. These combinations of item names and prefixes used to locate items are sometimes called paths:

  • Absolute Paths: Start from the crate root or from an external, named crate.
  • Relative Paths: Start from the current module using self or super.

Absolute Paths

Absolute paths begin from the crate root or an external crate.

Example:

crate::module::submodule::function();
std::collections::HashMap::new();

Relative Paths

Relative paths begin from the current module using self or super.

  • self: Refers to the current module.
  • super: Refers to the parent module.

Example:

mod parent {
    pub mod child {
        pub fn function() {
            println!("In child module.");
        }
        pub fn call_parent() {
            super::parent_function(); // Call private function of parent module
        }
    }
    fn parent_function() {
        println!("In parent module.");
    }
}
fn main() {
    parent::child::function();
    parent::child::call_parent();
}

17.3.6 The use Keyword in Detail

Within a scope, the use declaration can be used to bind a full path to a new name, creating a shortcut for accessing items directly by name or via shorter paths.

While use can reduce verbosity and improve code clarity, overusing it can obscure the origin of imported items and increase the risk of name collisions. A common practice is to use use for bringing data types into scope unqualified (e.g., HashMap), while retaining a module prefix for functions like io::read_to_string().

Additionally, use is mandatory to bring external crates into scope.

Importing Symbols

The use keyword can bring specific items into scope, enabling direct access without their full paths.

use std::collections::HashMap;
fn main() {
    let mut map = HashMap::new(); // Shortened path enabled with `use`
    // let mut map = std::collections::HashMap::new(); // Fully qualified path
    map.insert(37, "b"); // Needed for type inference
}

What might be surprising is the fact that an item brought into scope with use is not available by default in a submodule. The following code does not compile, as the symbol HashMap is not declared inside module m. To fix this issue, we can move the use statement into the module m, or we can prefix the item HashMap with super::.

use std::collections::HashMap;
mod m {
    pub fn func() {
        let mut map: HashMap<i32, i32> = HashMap::new(); // Does not compile, use `super::HashMap` instead.
    }
}
fn main() {
    m::func();
}

Wildcard Imports

All public items of a module can be imported using a glob pattern (*).

use std::collections::*;

Wildcard imports are generally discouraged because they can make it harder to determine the origin of items and increase the likelihood of naming conflicts. However, they may be useful in prototyping or testing scenarios.

Importing Multiple Items with {}

You can import multiple items from a module in a single use statement.

use std::collections::{HashMap, HashSet};

The self keyword can be used to include the module itself:

use std::io::{self, Read}; // Equivalent to `use std::io; use std::io::Read;`

Aliasing Imports

Items can be renamed upon import to avoid conflicts or simplify names.

use std::collections::HashMap as Map;
fn main() {
    let mut map = Map::new(); // Alias used instead of `HashMap`
    map.insert(37, "b"); // Needed for type inference
}

Nested Paths

Rust allows combining multiple imports with shared prefixes into a single statement, simplifying the code.

// Importing items one by one
use std::cmp::Ordering;
use std::io;
use std::io::Write;

// Compact form using nested paths
use std::{cmp::Ordering, io::{self, Write}};

Local Imports

The use keyword can also be used inside functions to limit the scope of imports. This helps reduce global scope pollution and keeps imports specific to their context.

fn main() {
    use std::io::Write;

    let mut buffer = Vec::new();
    buffer.write_all(b"Hello, world!").unwrap();
}

17.3.7 Re-Exporting and Aliasing

Re-exporting makes items available as part of the public API of the parent module.

Re-exporting Example:

mod inner {
    pub fn inner_function() {
        println!("Inner function.");
    }
}
pub use inner::inner_function;
fn main() {
    inner_function();
}

Aliasing Re-exports Example:

pub use crate::inner::inner_function as public_function;

Now, public_function is available for external use.

17.3.8 Visibility Modifiers

For large projects with a lot of modules that depend on each other and might need common data types, Rust allows users to declare an item as visible only within a given scope. A common example is geometric data structures like meshes (e.g., the Delaunay triangulation) with Edge and Vertex data types that have to refer to each other. In these cases, cyclic imports—where module a imports items from module b, and b imports items from a—should typically be avoided. A possible solution is to create a module c containing common parts (data types), from which a and b import what is needed. Rust's pub modifiers offer another solution for such advanced use cases.

  • pub(in path) makes an item visible within the provided path. The path must be a simple path that resolves to an ancestor module of the item whose visibility is being declared. Each identifier in path must refer directly to a module (not to a name introduced by a use statement).
  • pub(crate) makes an item visible within the current crate.
  • pub(super) makes an item visible to the parent module. This is equivalent to pub(in super).
  • pub(self) makes an item visible to the current module. This is equivalent to pub(in self) or not using pub at all.

The Rust language reference provides a detailed explanation for these modifiers and has some examples.