Jump to content

Modules

From JOHNWICK
Revision as of 19:49, 23 November 2025 by PC (talk | contribs) (Created page with "I missed publishing yesterday’s writeup but here we are. If you missed our discussion on smart pointers check it out below. Smart Pointers Yesterday we covered Closures, why they are important and edge cases to watch when working with them. If you missed it… medium.com Rust’s module system is just about organizing your code into logical, reusable pieces. Think of it like organizing a messy toolbox, sockets in one drawer, wrenches in another, and screwdrivers neat...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

I missed publishing yesterday’s writeup but here we are. If you missed our discussion on smart pointers check it out below.

Smart Pointers Yesterday we covered Closures, why they are important and edge cases to watch when working with them. If you missed it… medium.com

Rust’s module system is just about organizing your code into logical, reusable pieces. Think of it like organizing a messy toolbox, sockets in one drawer, wrenches in another, and screwdrivers neatly arranged. Without modules, your growing codebase can becomes a chaotic mess and often youll need to separate functions.

Modules is a way to group related code, functions, structs, enums, and more, into a single unit. It’s like a folder in your project that holds pieces of functionality together. Modules control visibility (what’s public or private), help avoid naming conflicts, and make your code easier to navigate.

Imagine you’re working on a backend of a platform. You’ve got code for handling user accounts, processing payments, and managing inventory.

Without modules, all these functions and types would live in one giant file or directory, making it hard to find anything. Modules let you split this into users, payments, and inventory modules, so each part is self-contained and easier to work with.

Here’s a simple example of a module structure for such a backend.

mod users {
    pub struct User {
        id: u32,
        name: String,
    }

pub fn create_user(name: &str) -> User {
        User {
            id: generate_id(),
            name: name.to_string(),
        }
    }
    fn generate_id() -> u32 {
        // Simulate fetching an ID from a database
        42
    }
}
fn main() {
    let user = users::create_user("Alice");
    println!("User: {} (ID: {})", user.name, user.id);
}

In this code above, the users module groups user-related functionality. The pub keyword makes User and create_user accessible outside the module, while generate_id is private, hidden from the outside world.

Such a setup mirrors how you’d organize a real project, exposing only what’s necessary (like an API) while keeping internal details private.

Declaring and Using Modules

Modules can be defined in a single file using the mod keyword, as shown in the code above, or split across multiple files for larger projects. Let’s say your project grows, and you want to separate the users module into its own file. Here’s how you’d do it. src/ ├── main.rs ├── users.rs

In main.rs, you declare the module:

// Declares the users module, which Rust looks for in users.rs

mod users;

fn main() {
    let user = users::create_user("Bob");
    println!("User: {} (ID: {})", user.name, user.id);
}
And in users.rs, you define the module:
pub struct User {
    pub id: u32,
    pub name: String,
}

pub fn create_user(name: &str) -> User {
    User {
        id: generate_id(),
        name: name.to_string(),
    }
}

fn generate_id() -> u32 {
    42 // Simulate database ID generation
}

When you run main.rs, Rust automatically loads the users module from users.rs. This is a common pattern, where each module lives in its own file to keep things organized.

For example, a payment processing team might work on payments.rs, while the inventory team works on inventory.rs, and main.rs ties it all together. The connection here is seamless , you just use users::create_user to access the function.

Visibility and Encapsulation

One of the most practical aspects of modules is controlling visibility with pub. In some scenario where you’re building a library or API and some parts should be exposed to users, but others should remain internal.

For instance, in the users module above, create_user and User are public, but generate_id is private because it’s an implementation detail. Consider a payment processing module below.

mod payments {

    pub fn process_payment(amount: f64, user_id: u32) -> Result<String, String> {
        if validate_payment(amount, user_id) {
            Ok(format!("Payment of {} processed for user {}", amount, user_id))
        } else {
            Err("Payment validation failed".to_string())
        }
    }

  fn validate_payment(amount: f64, user_id: u32) -> bool {
        // Internal logic to check payment details
        amount > 0.0 && user_id != 0
      }
}

In the code above, process_payment is public, so other parts of the program (or external users, if it’s a library) can call that function. But validate_payment is private, keeping the validation logic hidden. This can be critical in cases, where you want to protect internal logic from misuse or accidental dependency.

Nested Modules and Paths

Another nice thing is that modules can be nested to create a hierarchy, which is super useful for organizing complex projects.

Suppose we want to extend our users module with submodules for authentication and profiles:

mod users {
    pub mod auth {
        pub fn login(username: &str, password: &str) -> bool {
            // Simulate authentication here
            username == "admin" && password == "secret"
        }
    }

pub mod profile {
        pub struct Profile {
            pub bio: String,
            pub email: String,
        }
        pub fn update_profile(user_id: u32, bio: &str, email: &str) -> Profile {
            Profile {
                bio: bio.to_string(),
                email: email.to_string(),
            }
        }
    }
}

fn main() {
    if users::auth::login("admin", "secret") {
        let profile = users::profile::update_profile(42, "Rust enthusiast", "[email protected]");
        println!("Profile updated: {} ({})", profile.bio, profile.email);
    }
}

In our code above, users contains auth and profile submodules. You access them with paths like users::auth::login. This can be common in large projects, like a web framework where you might have framework::http::request and framework::http::response.

Nested modules keep related functionality grouped while allowing fine-grained control over visibility. In a real job, for example, if you’re tasked for example with building the authentication system, you’d work in the auth submodule, while someone else handles profile.

The module hierarchy ensures your changes don’t accidentally break other parts of the system.

The use Keyword and Path Management

More often as project grows, typing long paths like users::auth::login can get tedious. The use keyword lets you bring items into scope for cleaner code. Here’s an example:

use users::auth::login;
use users::profile::update_profile;

fn main() {
    if login("admin", "secret") {
        let profile = update_profile(42, "Rust lover", "[email protected]");
        println!("Profile: {} ({})", profile.bio, profile.email);
    }
}

See in our code above how we call login and update_profile directly. This is a lifesaver in real-world projects with deep module hierarchies. You can also use use to create aliases or avoid naming conflicts. Imagine two modules with conflicting function names:

mod inventory {
    pub fn update(id: u32) {
        println!("Updating inventory item {}", id);
    }
}

mod orders {
    pub fn update(id: u32) {
        println!("Updating order {}", id);
    }
}

use inventory::update as update_inventory;
use orders::update as update_order;

fn main() {
    update_inventory(123);
    update_order(456);
}

The code above could be in a case when integrating third-party libraries or working on a team where different modules might have functions with the same name. Aliases keep things clear and prevent errors. Here are some tips for using modules effectively in real-world Rust projects:

  • Each module should have a clear purpose, like users::auth for authentication or payments for payment processing. This makes it easier for team members to find and work on specific features.
  • Break down complex modules into submodules. For example, a database module might have query, connection, and schema submodules to separate concerns.
  • Be intentional about pub. Expose only what’s necessary to prevent external code from depending on unstable internal functions. For instance, in the logger example, get_timestamp is private to avoid external misuse.
  • In large projects, place each module in its own file or directory. For example, a database module might have a directory with mod.rs, query.rs, and connection.rs for clarity.
  • Write unit tests for each module in its file (e.g., in users.rs for the users module). This ensures bugs in one module don’t affect others, a common issue in collaborative projects.

That was all for today.

Read the full article here: https://medium.com/rustaceans/modules-7afe20b7f6e1