Understanding the Basics of Rust’s Module System

At its core, Rust’s module system is about organizing code and controlling visibility. Let’s start with the fundamental concepts:

Modules: Organizing Code into Logical Units

A module is a container for items such as functions, structs, traits, impl blocks, and even other modules:

// Define a module named 'geometry"
mod geometry {
    // Constants within the module
    const PI: f64 = 3.14159;
    
    // Functions within the module
    pub fn area_of_circle(radius: f64) -> f64 {
        PI * radius * radius
    }
    
    // Nested module
    pub mod shapes {
        pub struct Rectangle {
            pub width: f64,
            pub height: f64,
        }
        
        impl Rectangle {
            pub fn new(width: f64, height: f64) -> Rectangle {
                Rectangle { width, height }
            }
            
            pub fn area(&self) -> f64 {
                self.width * self.height
            }
        }
    }
}

fn main() {
    // Using items from the module
    let area = geometry::area_of_circle(5.0);
    println!("Area of circle: {}", area);
    
    let rect = geometry::shapes::Rectangle::new(10.0, 5.0);
    println!("Area of rectangle: {}", rect.area());
}

Visibility and the pub Keyword

By default, everything in Rust is private. The pub keyword makes items accessible outside their defining module:

mod math {
    // Private function, only accessible within this module
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    // Public function, accessible from outside the module
    pub fn multiply(a: i32, b: i32) -> i32 {
        a * b
    }
    
    // Public function that uses a private function
    pub fn add_and_multiply(a: i32, b: i32, c: i32) -> i32 {
        // We can call add() here because we're in the same module
        multiply(add(a, b), c)
    }
}

fn main() {
    // math::add(1, 2); // Error: add is private
    let result = math::multiply(2, 3);
    println!("2 * 3 = {}", result);
    
    let result = math::add_and_multiply(1, 2, 3);
    println!("(1 + 2) * 3 = {}", result);
}

Paths: Referring to Items in the Module Tree

Rust uses paths to refer to items in the module tree:

mod outer {
    pub mod inner {
        pub fn function() {
            println!("This is a function in the inner module");
        }
    }
}

fn main() {
    // Absolute path
    crate::outer::inner::function();
    
    // Relative path
    outer::inner::function();
}

The use Keyword: Bringing Paths into Scope

The use keyword brings items into scope to avoid repeating long paths:

mod shapes {
    pub struct Circle {
        pub radius: f64,
    }
    
    pub struct Rectangle {
        pub width: f64,
        pub height: f64,
    }
}

use shapes::Circle;
use shapes::Rectangle;

// Alternatively, using a single use statement:
// use shapes::{Circle, Rectangle};

fn main() {
    let circle = Circle { radius: 5.0 };
    let rectangle = Rectangle { width: 10.0, height: 5.0 };
    
    println!("Circle radius: {}", circle.radius);
    println!("Rectangle dimensions: {} x {}", rectangle.width, rectangle.height);
}

Module Organization in Files and Directories

As projects grow, organizing modules across files becomes essential. Rust provides several ways to structure your code:

Single-File Modules

For small projects, you can define all modules in a single file:

// src/main.rs
mod config {
    pub struct Config {
        pub api_key: String,
        pub timeout: u32,
    }
    
    impl Config {
        pub fn new(api_key: String, timeout: u32) -> Config {
            Config { api_key, timeout }
        }
    }
}

mod api {
    use super::config::Config;
    
    pub struct Client {
        config: Config,
    }
    
    impl Client {
        pub fn new(config: Config) -> Client {
            Client { config }
        }
        
        pub fn send_request(&self, endpoint: &str) {
            println!(
                "Sending request to {} with API key {} and timeout {}",
                endpoint,
                self.config.api_key,
                self.config.timeout
            );
        }
    }
}

use config::Config;
use api::Client;

fn main() {
    let config = Config::new(String::from("my-api-key"), 30);
    let client = Client::new(config);
    client.send_request("/users");
}

Modules in Separate Files

For larger projects, you can split modules into separate files:

// src/main.rs
mod config; // Declares the module and tells Rust to look for its contents in another file
mod api;

use config::Config;
use api::Client;

fn main() {
    let config = Config::new(String::from("my-api-key"), 30);
    let client = Client::new(config);
    client.send_request("/users");
}

// src/config.rs
pub struct Config {
    pub api_key: String,
    pub timeout: u32,
}

impl Config {
    pub fn new(api_key: String, timeout: u32) -> Config {
        Config { api_key, timeout }
    }
}

// src/api.rs
use crate::config::Config;

pub struct Client {
    config: Config,
}

impl Client {
    pub fn new(config: Config) -> Client {
        Client { config }
    }
    
    pub fn send_request(&self, endpoint: &str) {
        println!(
            "Sending request to {} with API key {} and timeout {}",
            endpoint,
            self.config.api_key,
            self.config.timeout
        );
    }
}

Modules in Directories

For even more complex projects, you can organize modules in directories:

src/
├── main.rs
├── config/
│   ├── mod.rs
│   └── types.rs
└── api/
    ├── mod.rs
    ├── client.rs
    └── response.rs
// src/main.rs
mod config;
mod api;

use config::Config;
use api::Client;

fn main() {
    let config = Config::new(String::from("my-api-key"), 30);
    let client = Client::new(config);
    let response = client.send_request("/users");
    println!("Response status: {}", response.status);
}

// src/config/mod.rs
mod types; // Submodule

pub use types::ConfigType;

pub struct Config {
    pub api_key: String,
    pub timeout: u32,
    pub config_type: ConfigType,
}

impl Config {
    pub fn new(api_key: String, timeout: u32) -> Config {
        Config {
            api_key,
            timeout,
            config_type: ConfigType::Production,
        }
    }
}

// src/config/types.rs
pub enum ConfigType {
    Development,
    Testing,
    Production,
}

// src/api/mod.rs
mod client;
mod response;

pub use client::Client;
pub use response::Response;

// src/api/client.rs
use crate::config::Config;
use super::response::Response;

pub struct Client {
    config: Config,
}

impl Client {
    pub fn new(config: Config) -> Client {
        Client { config }
    }
    
    pub fn send_request(&self, endpoint: &str) -> Response {
        println!(
            "Sending request to {} with API key {} and timeout {}",
            endpoint,
            self.config.api_key,
            self.config.timeout
        );
        
        Response { status: 200, body: String::from("Success") }
    }
}

// src/api/response.rs
pub struct Response {
    pub status: u32,
    pub body: String,
}