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 can only be accessed by code within the same module or in direct child modules.

To make an item accessible from outside its defining module, you must mark it with the pub (public) keyword.

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 direct children modules (for items defined within the parent). 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.
  • pub(crate): Visible anywhere within the same crate, but not outside the crate. Useful for internal helper functions or types shared across different modules of the crate but not part of its public API.
  • 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.

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
    pub struct AppConfig {
        // Field is public
        pub server_address: String,
        // Field is private (only accessible within 'configuration' module)
        api_secret: String,
        // Field is crate-visible
        pub(crate) 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,
                max_retries: 5, // Default internal value
            }
        }

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

        // Crate-visible method (could be used by other modules in this crate)
        pub(crate) fn set_max_retries(&mut self, retries: u32) {
            self.max_retries = retries;
        }
    }

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

fn main() {
    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

    // OK: max_retries is pub(crate), accessible within the same crate
    println!("Max Retries (initial): {}", config.max_retries);
    // config.set_max_retries(10); // We could call this if it were pub(crate)
    // Direct access would also work if main was in the same crate:
    // config.max_retries = 10;
    // println!("Max Retries (updated): {}", config.max_retries);


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

    // OK: Access via public method
    println!("{}", config.get_secret_info());

    // OK: Use public enum variant
    let level = configuration::LogLevel::Warning;
}

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.
    mod outer {
        pub fn outer_func() { println!("Outer function"); }
        pub mod inner {
            pub fn inner_func() {
                println!("Inner function calling sibling:");
                self::sibling_func(); // Call function in same module ('inner')
                println!("Inner function calling parent:");
                super::outer_func(); // Call function in parent module ('outer')
            }
            pub fn sibling_func() { println!("Sibling function"); }
        }
    }
    fn main() { outer::inner::inner_func(); }

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;

// Bring the connect function from our hypothetical network::client module
// Assume 'network' module is declared earlier or in another file
mod network { pub mod client { pub fn connect() { /* ... */ } } }
use crate::network::client::connect;

fn main() {
    // Now we can use HashMap and connect directly
    let mut scores = HashMap::new();
    scores.insert("Alice", 100);
    connect();
    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: Often idiomatic to import the function’s full path.
    use crate::network::client::connect;
    connect(); // Call directly
  • 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: If importing two items with the same name, you can either import their parent modules and use full paths, or use as to rename one or both imports.
    use std::fmt::Result as FmtResult; // Rename std::fmt::Result
    use std::io::Result as IoResult;   // Rename std::io::Result
    
    fn function_one() -> FmtResult {
        // ... implementation returning std::fmt::Result ...
        Ok(())
    }
    
    fn function_two() -> IoResult<()> {
        // ... implementation returning std::io::Result ...
        Ok(())
    }
    fn main() { function_one().unwrap(); function_two().unwrap(); }

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::*;.