Rust - Modules & Packages

Overview

Estimated time: 60–75 minutes

Learn Rust's module system for code organization, including visibility rules, package structure, and workspace management. Master organizing code from small programs to large-scale projects.

Learning Objectives

Prerequisites

The Module System

Basic Module Declaration

Modules organize code into logical groups with mod:

// Basic module with functions
mod math_utils {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    pub fn multiply(a: i32, b: i32) -> i32 {
        a * b
    }
    
    // Private function (not accessible outside module)
    fn internal_calculation() -> i32 {
        42
    }
}

fn main() {
    let sum = math_utils::add(5, 3);
    let product = math_utils::multiply(4, 7);
    
    println!("Sum: {}", sum);         // Sum: 8
    println!("Product: {}", product); // Product: 28
    
    // This would cause a compile error:
    // math_utils::internal_calculation(); // private function
}

Nested Modules

Modules can contain other modules for hierarchical organization:

mod geometry {
    pub mod shapes {
        pub struct Circle {
            pub radius: f64,
        }
        
        pub struct Rectangle {
            pub width: f64,
            pub height: f64,
        }
        
        impl Circle {
            pub fn new(radius: f64) -> Self {
                Circle { radius }
            }
            
            pub fn area(&self) -> f64 {
                3.14159 * self.radius * self.radius
            }
        }
        
        impl Rectangle {
            pub fn new(width: f64, height: f64) -> Self {
                Rectangle { width, height }
            }
            
            pub fn area(&self) -> f64 {
                self.width * self.height
            }
        }
    }
    
    pub mod calculations {
        use super::shapes::{Circle, Rectangle};
        
        pub fn total_area(circle: &Circle, rect: &Rectangle) -> f64 {
            circle.area() + rect.area()
        }
    }
}

fn main() {
    let circle = geometry::shapes::Circle::new(5.0);
    let rectangle = geometry::shapes::Rectangle::new(10.0, 8.0);
    
    println!("Circle area: {:.2}", circle.area());                    // Circle area: 78.54
    println!("Rectangle area: {:.2}", rectangle.area());              // Rectangle area: 80.00
    
    let total = geometry::calculations::total_area(&circle, &rectangle);
    println!("Total area: {:.2}", total);                             // Total area: 158.54
}

Visibility and Privacy

Public vs Private

Items are private by default. Use pub to make them public:

mod bank {
    pub struct Account {
        pub owner: String,
        balance: f64,  // Private field
    }
    
    impl Account {
        pub fn new(owner: String, initial_balance: f64) -> Self {
            Account {
                owner,
                balance: initial_balance,
            }
        }
        
        pub fn balance(&self) -> f64 {
            self.balance
        }
        
        pub fn deposit(&mut self, amount: f64) {
            if amount > 0.0 {
                self.balance += amount;
            }
        }
        
        pub fn withdraw(&mut self, amount: f64) -> bool {
            if amount > 0.0 && amount <= self.balance {
                self.balance -= amount;
                true
            } else {
                false
            }
        }
        
        // Private method
        fn validate_transaction(&self, amount: f64) -> bool {
            amount > 0.0 && amount <= 10000.0
        }
    }
}

fn main() {
    let mut account = bank::Account::new("Alice".to_string(), 1000.0);
    
    println!("Owner: {}", account.owner);           // Owner: Alice
    println!("Balance: ${:.2}", account.balance()); // Balance: $1000.00
    
    account.deposit(500.0);
    println!("After deposit: ${:.2}", account.balance()); // After deposit: $1500.00
    
    if account.withdraw(200.0) {
        println!("Withdrawal successful");
    }
    
    // These would cause compile errors:
    // println!("{}", account.balance);        // balance field is private
    // account.validate_transaction(100.0);    // private method
}

Different Levels of Visibility

Rust provides fine-grained visibility control:

mod company {
    pub struct Employee {
        pub name: String,
        pub(crate) id: u32,           // Visible within crate
        pub(super) department: String, // Visible to parent module
        salary: f64,                   // Private
    }
    
    impl Employee {
        pub fn new(name: String, id: u32, department: String, salary: f64) -> Self {
            Employee { name, id, department, salary }
        }
        
        pub fn get_info(&self) -> String {
            format!("{} (ID: {}, Dept: {})", self.name, self.id, self.department)
        }
    }
    
    pub mod hr {
        use super::Employee;
        
        pub fn print_employee_info(emp: &Employee) {
            println!("Name: {}", emp.name);
            println!("ID: {}", emp.id);                    // pub(crate) - accessible
            println!("Department: {}", emp.department);     // pub(super) - accessible
            // println!("Salary: {}", emp.salary);         // private - not accessible
        }
    }
}

fn main() {
    let employee = company::Employee::new(
        "Bob Smith".to_string(),
        12345,
        "Engineering".to_string(),
        75000.0
    );
    
    println!("{}", employee.get_info());           // Bob Smith (ID: 12345, Dept: Engineering)
    company::hr::print_employee_info(&employee);
    
    // These are accessible based on visibility:
    println!("Name: {}", employee.name);           // pub - accessible
    println!("ID: {}", employee.id);               // pub(crate) - accessible within crate
    // println!("Dept: {}", employee.department);  // pub(super) - not accessible here
}

Use Statements and Path Resolution

Bringing Items into Scope

Use use statements to avoid repetitive path writing:

mod restaurant {
    pub mod front_of_house {
        pub mod hosting {
            pub fn add_to_waitlist() {
                println!("Adding customer to waitlist");
            }
            
            pub fn seat_at_table() {
                println!("Seating customer at table");
            }
        }
        
        pub mod serving {
            pub fn take_order() {
                println!("Taking customer order");
            }
            
            pub fn serve_order() {
                println!("Serving order to customer");
            }
        }
    }
}

// Different ways to use items
use restaurant::front_of_house::hosting;
use restaurant::front_of_house::serving::take_order;

fn main() {
    // Using full path
    restaurant::front_of_house::hosting::add_to_waitlist();
    
    // Using shortened path after 'use'
    hosting::seat_at_table();
    
    // Using directly after 'use'
    take_order();
    
    // Can still use full path
    restaurant::front_of_house::serving::serve_order();
}

Use with Aliases and Wildcards

Advanced use patterns for flexibility:

mod collections {
    pub mod vectors {
        pub fn create_vec() -> Vec {
            vec![1, 2, 3, 4, 5]
        }
        
        pub fn process_vec(v: &Vec) {
            println!("Processing vector: {:?}", v);
        }
    }
    
    pub mod hashmaps {
        use std::collections::HashMap;
        
        pub fn create_map() -> HashMap {
            let mut map = HashMap::new();
            map.insert("one".to_string(), 1);
            map.insert("two".to_string(), 2);
            map
        }
    }
}

// Using aliases to avoid naming conflicts
use collections::vectors::create_vec as create_vector;
use collections::hashmaps::create_map as create_hashmap;

// Using wildcards (use sparingly)
use std::collections::*;

fn main() {
    let vec = create_vector();
    collections::vectors::process_vec(&vec);
    
    let map = create_hashmap();
    println!("Map: {:?}", map);
    
    // Using wildcard import
    let mut set = HashSet::new();
    set.insert("hello");
    set.insert("world");
    println!("Set: {:?}", set);
}

File-Based Modules

Organizing Modules in Files

For larger projects, separate modules into files:

// main.rs
mod calculator;  // Declares calculator module (looks for calculator.rs)

use calculator::basic::{add, subtract};
use calculator::scientific::power;

fn main() {
    println!("5 + 3 = {}", add(5, 3));           // 5 + 3 = 8
    println!("10 - 4 = {}", subtract(10, 4));    // 10 - 4 = 6
    println!("2^8 = {}", power(2.0, 8.0));       // 2^8 = 256
}

// calculator.rs (or calculator/mod.rs)
pub mod basic {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
    
    pub fn subtract(a: i32, b: i32) -> i32 {
        a - b
    }
    
    pub fn multiply(a: i32, b: i32) -> i32 {
        a * b
    }
    
    pub fn divide(a: i32, b: i32) -> Option {
        if b != 0 {
            Some(a / b)
        } else {
            None
        }
    }
}

pub mod scientific {
    pub fn power(base: f64, exponent: f64) -> f64 {
        base.powf(exponent)
    }
    
    pub fn sqrt(value: f64) -> f64 {
        value.sqrt()
    }
    
    pub fn log(value: f64, base: f64) -> f64 {
        value.log(base)
    }
}

Packages and Crates

Package Structure

Understanding Rust's package system with Cargo.toml:

# Cargo.toml
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = "1.0"
tokio = { version = "1.0", features = ["full"] }

[dev-dependencies]
criterion = "0.4"

[[bin]]
name = "main"
path = "src/main.rs"

[[bin]]
name = "helper"
path = "src/bin/helper.rs"

[lib]
name = "my_project"
path = "src/lib.rs"

Library and Binary Organization

Typical project structure for libraries and binaries:

// src/lib.rs - Library root
pub mod config;
pub mod database;
pub mod api;

pub use config::Config;
pub use database::Database;

pub fn initialize() -> Result<(), Box> {
    println!("Initializing library...");
    Ok(())
}

// src/main.rs - Binary using the library
use my_project::{initialize, Config, Database};

fn main() -> Result<(), Box> {
    initialize()?;
    
    let config = Config::new("config.toml")?;
    let mut db = Database::connect(&config.db_url)?;
    
    println!("Application started successfully");
    Ok(())
}

// src/config.rs
use std::fs;

pub struct Config {
    pub db_url: String,
    pub port: u16,
}

impl Config {
    pub fn new(path: &str) -> Result> {
        let contents = fs::read_to_string(path)?;
        // Parse configuration (simplified)
        Ok(Config {
            db_url: "localhost:5432".to_string(),
            port: 8080,
        })
    }
}

// src/database.rs
pub struct Database {
    connection_string: String,
}

impl Database {
    pub fn connect(url: &str) -> Result> {
        println!("Connecting to database: {}", url);
        Ok(Database {
            connection_string: url.to_string(),
        })
    }
    
    pub fn query(&self, sql: &str) -> Result, Box> {
        println!("Executing query: {}", sql);
        Ok(vec!["result1".to_string(), "result2".to_string()])
    }
}

Workspaces

Multi-Crate Workspaces

Organizing multiple related crates in a workspace:

# Root Cargo.toml
[workspace]
members = [
    "core",
    "cli",
    "web",
    "shared"
]

resolver = "2"

[workspace.dependencies]
serde = "1.0"
tokio = "1.0"
// core/src/lib.rs
pub mod engine {
    pub fn process_data(input: &str) -> String {
        format!("Processed: {}", input)
    }
}

pub mod utils {
    pub fn format_output(data: &str) -> String {
        format!("[OUTPUT] {}", data)
    }
}

// cli/src/main.rs
use core::engine::process_data;
use core::utils::format_output;

fn main() {
    let input = "Hello, workspace!";
    let processed = process_data(input);
    let formatted = format_output(&processed);
    
    println!("{}", formatted);  // [OUTPUT] Processed: Hello, workspace!
}

// web/src/lib.rs
use core::engine::process_data;

pub fn handle_request(input: &str) -> String {
    let result = process_data(input);
    format!("{{\"result\": \"{}\"}}", result)
}

Common Pitfalls

⚠️ Module Path Confusion

Understanding the difference between module paths and file paths:

// ❌ Wrong - this tries to use filesystem path
use "./utils/helper.rs";

// ✅ Correct - use module path
mod utils;
use utils::helper;

// ❌ Wrong - assuming file structure matches module structure
mod database::connection;  // Only works if database is already declared

// ✅ Correct - declare parent module first
mod database;
use database::connection;

⚠️ Circular Dependencies

Avoid circular module dependencies:

// ❌ Wrong - circular dependency
// In module A:
use crate::B::function_b;

// In module B:
use crate::A::function_a;  // Creates circular dependency

// ✅ Better - extract shared functionality
// Create module C with shared functionality
// Both A and B use C, avoiding circular dependency

Best Practices

Module Organization Guidelines

// Good module organization example
pub mod user {
    mod validation;
    mod storage;
    
    pub struct User {
        pub username: String,
        email: String,  // Private - access through methods
    }
    
    // Clean public API
    impl User {
        pub fn new(username: String, email: String) -> Result {
            validation::validate_email(&email)?;
            validation::validate_username(&username)?;
            
            Ok(User { username, email })
        }
        
        pub fn email(&self) -> &str {
            &self.email
        }
        
        pub fn save(&self) -> Result<(), StorageError> {
            storage::save_user(self)
        }
    }
    
    // Re-export only what users need
    pub use validation::ValidationError;
    pub use storage::StorageError;
}

Checks for Understanding

Question 1: What's the difference between pub, pub(crate), and pub(super)?

Answer:

  • pub - Public to all code that can access the parent module
  • pub(crate) - Public only within the current crate
  • pub(super) - Public only to the parent module
  • No modifier (private) - Only accessible within the same module
Question 2: How do you organize a module across multiple files?

Answer: Use mod module_name; to declare the module, then create either:

  • module_name.rs file for simple modules
  • module_name/mod.rs directory with mod.rs for complex modules with submodules

The module declaration looks for these files automatically.

Question 3: What's the purpose of workspaces in Rust?

Answer: Workspaces allow you to:

  • Organize related crates together
  • Share dependencies and build artifacts
  • Run commands across all workspace members
  • Maintain consistent dependency versions
  • Develop multiple related packages together

Summary

Rust's module system provides powerful tools for code organization:


← PreviousNext →