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
- Understand Rust's module system with
mod
and visibility rules - Organize code with packages, crates, and workspaces
- Use
use
statements for importing and path resolution - Structure libraries and binary applications effectively
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
- Group related functionality in modules
- Use descriptive module names that clearly indicate purpose
- Keep modules focused on a single responsibility
- Make APIs minimal - only expose what users need
- Use re-exports to create clean public APIs
// 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 modulepub(crate)
- Public only within the current cratepub(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 modulesmodule_name/mod.rs
directory withmod.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:
- Modules organize code with
mod
and visibility controls - Packages and crates structure larger applications
- Use statements bring items into scope efficiently
- Workspaces manage multiple related crates
- Visibility rules control API boundaries and encapsulation