Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

17.3 Modules: Organizing Code Within a Crate

While packages and crates define compilation boundaries and dependency management, modules provide the mechanism for organizing code inside a single crate. Modules allow you to:

  1. Group related code: Place functions, structs, enums, traits, and constants related to a specific piece of functionality together.
  2. Control visibility (privacy): Define which items are accessible from outside the module.
  3. Create a hierarchical namespace: Avoid naming conflicts by nesting modules.

This system is Rust’s answer to namespace management and encapsulation, somewhat analogous to C++ namespaces or the C practice of using static to limit symbol visibility to a single file, but with more explicit compiler enforcement and finer-grained control.

17.3.1 Module Basics and Visibility

Items defined within a module (or at the crate root) are private by default. Private items are only accessible from within their defining module and its descendant modules. This hierarchical rule also implies that code in a module cannot access private items defined in its sibling modules (modules declared within the same parent); visibility for private items flows down the tree, not sideways.

To make an item accessible from outside its defining module, you must mark it with the pub (public) keyword. An item marked pub is accessible from its parent module and any scope that can reach the parent module. For example, if you declare mod my_module { pub fn my_function() {} } at the crate root (main.rs or lib.rs), code directly in the crate root can call my_module::my_function(), even though my_module itself isn’t marked pub. Marking the module pub mod my_module { ... } is necessary only if you want code outside the parent (e.g., in another crate if the parent is the crate root) to be able to traverse my_module’s path to access its public items.

Code in one module refers to items in another module using paths, like module_name::item_name or crate::module_name::item_name. The use keyword simplifies access by bringing items into the current scope.

17.3.2 Defining Modules: Inline vs. Files

Modules can be defined in two primary ways:

1. Inline Modules

Defined directly within a source file using the mod keyword followed by the module name and curly braces {} containing the module’s content.

// Crate root (e.g., main.rs or lib.rs)

// Define an inline module named 'networking'
mod networking {
    // This function is public *within* the 'networking' module
    // and accessible from outside if 'networking' itself is reachable.
    pub fn connect() {
        // Call a private helper function within the same module
        establish_connection();
        println!("Connected!");
    }

    // This function is private to the 'networking' module
    fn establish_connection() {
        println!("Establishing connection...");
        // Implementation details...
    }
}

fn main() {
    // Call the public function using its full path
    networking::connect();

    // This would fail compilation because establish_connection is private:
    // networking::establish_connection();
}

2. Modules in Separate Files

For better organization, especially with larger modules, their content is placed in separate files. You declare the module’s existence in its parent module (or the crate root) using mod module_name; (without braces). The compiler then looks for the module’s content based on standard conventions:

  • Convention 1 (Modern, Recommended): Look for src/module_name.rs.
  • Convention 2 (Older): Look for src/module_name/mod.rs.

Example (using src/networking.rs):

Project Structure:

my_crate/
├── src/
│   ├── main.rs         # Crate root
│   └── networking.rs   # Contains the 'networking' module content
└── Cargo.toml

src/main.rs:

// Declare the 'networking' module.
// The compiler looks for src/networking.rs or src/networking/mod.rs
mod networking; // Semicolon indicates content is in another file

fn main() {
    networking::connect();
}

src/networking.rs:

#![allow(unused)]
fn main() {
// Contents of the 'networking' module

pub fn connect() {
    establish_connection();
    println!("Connected!");
}

fn establish_connection() {
    println!("Establishing connection...");
    // Implementation details...
}
}

17.3.3 Submodules and File Structure

Modules can be nested to create hierarchies. If a module parent contains a submodule child, the file structure conventions extend naturally.

Modern Style (Recommended): If src/parent.rs contains pub mod child;, the compiler looks for the child module’s content in src/parent/child.rs.

my_crate/
├── src/
│   ├── main.rs         # Crate root, declares 'mod network;'
│   ├── network.rs      # Declares 'pub mod client;'
│   └── network/        # Directory for submodules of 'network'
│       └── client.rs   # Contains content of 'network::client' module
└── Cargo.toml

src/main.rs:

mod network; // Looks for src/network.rs

fn main() {
    // Assuming connect is pub in client, and client is pub in network
    network::client::connect();
}

src/network.rs:

// Declare the 'client' submodule. Make it public ('pub mod') if it needs
// to be accessible from outside the 'network' module (e.g., from main.rs).
// Looks for src/network/client.rs
pub mod client;

// Other items specific to the 'network' module could go here.
// E.g., pub(crate) struct SharedNetworkState { ... }

src/network/client.rs:

#![allow(unused)]
fn main() {
// Contents of the 'network::client' module
pub fn connect() {
    println!("Connecting via network client...");
}
}

Older Style (Using mod.rs): If src/parent/mod.rs contains pub mod child;, the compiler looks for the child module’s content in src/parent/child.rs.

my_crate/
├── src/
│   ├── main.rs         # Crate root, declares 'mod network;'
│   └── network/        # Directory for 'network' module
│       ├── mod.rs      # Contains 'network' content, declares 'pub mod client;'
│       └── client.rs   # Contains content of 'network::client' module
└── Cargo.toml

While both styles are supported, the non-mod.rs style (network.rs + network/client.rs) is generally preferred for new projects. It avoids having many files named mod.rs, making navigation potentially easier, as the file name directly matches the module name. Consistency within a project is the most important aspect.

17.3.4 Controlling Visibility with pub

Rust’s visibility rules provide fine-grained control, defaulting to private for strong encapsulation.

  • private (default, no keyword): Accessible only within the current module and its descendant modules. Think of it like C’s static for functions/variables within a file, but applied to all items and enforced hierarchically.
  • pub: Makes the item public. If an item is pub, it’s accessible from anywhere its parent module is accessible. For code outside the current crate to access a pub item, the entire path to the item (including all parent modules) must also be pub.
  • pub(crate): Visible within the same crate, but not outside the crate. It’s ideal for internal APIs that are shared across modules but not exposed to users of the crate.
  • pub(super): Visible only in the immediate parent module.
  • pub(in path::to::module): Visible only within the specified module path (which must be an ancestor module). This is less common but offers precise scoping.

Path Visibility Rule: For any item to be accessible, every module in its path, from the crate root down to the item itself, must be visible from the point of access. Even if an item is pub or pub(crate), it cannot be accessed if one of its parent modules in the path is private relative to the accessing code’s scope. You must be able to traverse the entire path.

Visibility of Struct Fields and Enum Variants:

  • Marking a struct or enum as pub makes the type itself public, but its contents follow their own rules:
    • Struct Fields: Fields are private by default, even if the struct itself is pub. You must explicitly mark fields with pub (or pub(crate), etc.) if you want code outside the module to access or modify them directly. This encourages using methods for interaction (encapsulation).
    • Enum Variants: Variants of a pub enum are public by default. If the enum type is accessible, all its variants are also accessible.
pub mod configuration {
    // Struct is public - the type AppConfig can be named outside this module.
    pub struct AppConfig {
        // Field is public - code outside can access config.server_address
        pub server_address: String,
        // Field is private (default) - only code in 'configuration' module
        // and its descendants can access config.api_secret directly.
        api_secret: String,
        // Field is private (default) - only code within the 'configuration' module
        // and its descendants can access config.max_retries directly.
        max_retries: u32,
    }

    impl AppConfig {
        // Public constructor (often named 'new')
        pub fn new(address: String, secret: String) -> Self {
            AppConfig {
                server_address: address,
                api_secret: secret, // Can access private field within the module
                max_retries: 5, // Can access private field within the module
            }
        }

        // Public method to access information derived from private field
        pub fn get_secret_info(&self) -> String {
            // Can access private field within the module
            format!("Secret length: {}", self.api_secret.len())
        }

        // Crate-visible method (could be used by other modules in this crate)
        // Note: This method provides controlled modification of the `max_retries`
        // field from elsewhere within the crate, even though the field itself is
        // private to this module.
        pub(crate) fn set_max_retries(&mut self, retries: u32) {
            self.max_retries = retries;
        }
        
        // Example of a pub(crate) getter for the now private max_retries field
        pub(crate) fn get_max_retries(&self) -> u32 {
            self.max_retries
        }
    }

    // Public enum
    pub enum LogLevel {
        Debug, // Variants are public because LogLevel is pub
        Info,
        Warning,
        Error,
    }
}

fn main() {
    // OK: AppConfig is pub, so we can use the type and its public constructor
    let mut config = configuration::AppConfig::new(
        "127.0.0.1:8080".to_string(),
        "super-secret-key".to_string()
    );

    // OK: server_address field is public
    println!("Server Address: {}", config.server_address);
    config.server_address = "192.168.1.100:9000".to_string(); // Modifiable

    // Error: max_retries field is private within the 'configuration' module.
    // Direct access from outside the module (like from main.rs) is not allowed.
    // println!("Max Retries (initial): {}", config.max_retries); // Compile error
    // config.max_retries = 10; // Compile error

    // OK: Use the pub(crate) method to modify the private max_retries field
    config.set_max_retries(10);
    // OK: Use the pub(crate) method to access the private max_retries field
    println!("Max Retries (updated via method): {}", config.get_max_retries());

    // Error: api_secret field is private within the 'configuration' module
    // println!("Secret: {}", config.api_secret); // Cannot access
    // config.api_secret = "new-secret".to_string(); // Cannot modify

    // OK: Access information derived from private field via a public method
    println!("{}", config.get_secret_info());

    // OK: Use public enum variant (since LogLevel is pub)
    let level = configuration::LogLevel::Warning;
    match level {
        configuration::LogLevel::Warning => println!("Log level is Warning"),
        _ => {},
    }
}

The example above illustrates an important concept: the visibility of a struct (pub struct AppConfig) determines whether the type can be used outside its module. The visibility of its fields (pub, pub(crate), or private) determines whether code outside the module can directly access those fields using dot notation (config.field_name). This allows creating public types that carefully control access to their internal data, a cornerstone of encapsulation.

17.3.5 Paths for Referring to Items

You use paths to refer to items (functions, types, modules) defined elsewhere.

  • Absolute Paths: Start from the crate root using the literal keyword crate:: or from an external crate’s name (e.g., rand::).
    crate::configuration::AppConfig::new(/* ... */); // Item in same crate
    std::collections::HashMap::new();               // Item in standard library
    rand::thread_rng();                             // Item in external 'rand' crate
  • Relative Paths: Start from the current module.
    • self::: Refers to an item within the current module (rarely needed unless disambiguating).
    • super::: Refers to an item within the parent module. Can be chained (super::super::) to go further up the hierarchy.

Choosing between absolute (crate::) and relative (super::) paths is often a matter of style and context. crate:: is unambiguous but can be longer. super:: is concise for accessing parent items but depends on the current module’s location.

17.3.6 Importing Items with use

Constantly writing long paths like std::collections::HashMap can be tedious. The use keyword brings items into the current scope, allowing you to refer to them directly by their final name.

// Bring HashMap from the standard library's collections module into scope
use std::collections::HashMap;

fn main() {
    // Now we can use HashMap directly
    let mut scores = HashMap::new();
    scores.insert("Alice", 100);
    println!("{:?}", scores);
}

Scope of use: A use declaration applies only to the scope it’s declared in (usually a module, but can also be a function or block). Siblings or parent modules are not affected; they need their own use declarations if they wish to import the same items.

Common use Idioms:

  • Functions: While you can import functions directly by their full path (use crate::network::client::connect; connect();), it is often considered more idiomatic to import the function’s parent module and then call the function using a qualified path (use crate::network::client; client::connect();). This approach makes the function’s origin more explicit at the call site and helps avoid name collisions.
    // Define a hypothetical module for demonstration
    mod networking {
        pub mod client {
            pub fn establish_connection() {
                println!("Connection established!");
            }
        }
    }
    
    // Less idiomatic: Direct import of the function
    use networking::client::establish_connection;
    
    // More idiomatic: Import the parent module and qualify the call
    use networking::client;
    
    fn main() {
        // Calling the directly imported function
        establish_connection();
    
        // Calling the function using the module-qualified path (idiomatic)
        client::establish_connection();
    }
  • Structs, Enums, Traits: Usually idiomatic to import the item itself.
    use std::collections::HashMap;
    let map = HashMap::new();
    
    use std::fmt::Debug;
    #[derive(Debug)] // Use the imported trait
    struct Point { x: i32, y: i32 }
  • Avoiding Name Conflicts / Aliasing: If importing items that would cause name collisions, or if you simply prefer a shorter or more descriptive name, you can use as to rename the imported item or module.
    // Renaming items (like types or functions)
    use std::fmt::Result as FmtResult; // Rename std::fmt::Result to FmtResult
    use std::io::Result as IoResult;   // Rename std::io::Result to IoResult
    
    // Renaming modules
    mod network { pub mod client { pub fn connect() {} } } // Hypothetical module
    use crate::network::client as net_client; // Rename module 'client' to 'net_client'
    
    fn main() {
        let _r1: FmtResult = Ok(()); // Use the aliased type
        let _r2: IoResult<()> = Ok(()); // Use the aliased type
        net_client::connect(); // Call through the aliased module name
    }

Nested Paths in use: Simplify importing multiple items from the same crate or module hierarchy.

// Instead of:
// use std::cmp::Ordering;
// use std::io;
// use std::io::Write;

// Use nested paths:
use std::{
    cmp::Ordering,
    io::{self, Write}, // Imports std::io, std::io::Write
};

// Or using 'self' for the parent module itself:
// use std::io::{self, Read, Write}; // Imports std::io, std::io::Read, std::io::Write

Glob Operator (*): The use path::*; syntax imports all public items from path into the current scope. While convenient, this is generally discouraged in library code and application logic because it makes it hard to determine where names originated and increases the risk of name collisions. Its primary legitimate use is often within prelude modules (see Section 17.3.9) or sometimes in tests.

17.3.7 Re-exporting with pub use

Sometimes, an item is defined deep within a module structure (e.g., crate::internal::details::UsefulType), but you want to expose it as part of your crate’s primary public API at a simpler path (e.g., crate::UsefulType). The pub use declaration allows you to re-export an item from another path, making it publicly available under the new path.

mod internal_logic {
    pub mod data_structures {
        pub struct ImportantData { pub value: i32 }
        pub fn process_data(data: &ImportantData) {
            println!("Processing data with value: {}", data.value);
        }
    }
}

// Re-export ImportantData and process_data at the crate root level.
// Users of this crate can now access them directly via `crate::`
pub use internal_logic::data_structures::{ImportantData, process_data};

// Optionally, re-export with a different name using 'as'
// pub use internal_logic::data_structures::ImportantData as PublicData;

fn main() {
    let data = ImportantData { value: 42 }; // Use the re-exported type
    process_data(&data);                    // Use the re-exported function
}

pub use is a powerful tool for designing clean, stable public APIs for libraries, hiding the internal module organization from users.

17.3.8 Overriding File Paths with #[path]

In rare situations, primarily when dealing with generated code or unconventional project layouts, the default module file path conventions (module_name.rs or module_name/mod.rs) might not apply. The #[path = "path/to/file.rs"] attribute allows you to explicitly tell the compiler where to find the source file for a module declared with mod.

// In src/main.rs or src/lib.rs

// Tell the compiler the 'config' module's code is in 'generated/configuration.rs'
#[path = "generated/configuration.rs"]
mod config;

fn main() {
    // Assuming 'load' is a public function in the 'config' module
    // config::load();
}

This attribute should be used sparingly as it deviates from standard Rust project structure.

17.3.9 The Prelude

Rust aims to keep the global namespace uncluttered. However, certain types, traits, and macros are so commonly used that requiring explicit use statements for them everywhere would be overly verbose. Rust addresses this with the prelude.

Every Rust module implicitly has access to the items defined in the standard library prelude (std::prelude::v1). This includes fundamental items like Option, Result, Vec, String, Box, common traits like Clone, Copy, Debug, Iterator, Drop, the vec! macro, and more. Anything not in the prelude must be explicitly imported using use.

Crates can also define their own preludes (often pub mod prelude { pub use ...; }) containing the most commonly used items from that crate, allowing users to import them conveniently with a single use my_crate::prelude::*;.