Rust Modules Basics
Introduction
Organizing code is essential for managing complexity in any programming project. As your Rust programs grow, you'll need a way to structure your code logically. This is where Rust's module system comes in.
Modules in Rust allow you to:
- Group related code together
- Control the privacy of items (public vs. private)
- Organize your code into a hierarchical structure
- Prevent naming conflicts
In this tutorial, we'll explore the basics of Rust's module system, how to create and use modules, and best practices for organizing your code.
What are Modules?
A module in Rust is a namespace that contains definitions of functions, structs, traits, impl blocks, or even other modules. Think of modules as containers that help you organize related code together and control how it's accessed from other parts of your program.
Creating Your First Module
Let's start with a simple example. We'll create a module called greetings
that contains various greeting functions.
// Define a module named "greetings"
mod greetings {
// Functions inside the module
fn say_hello() {
println!("Hello, world!");
}
fn say_goodbye() {
println!("Goodbye, world!");
}
}
fn main() {
// This won't work! These functions are private by default
// greetings::say_hello();
// greetings::say_goodbye();
}
If you try to run this code, you'll get an error because by default, everything inside a module is private. Let's fix that by making our functions public:
mod greetings {
// Use the "pub" keyword to make items public
pub fn say_hello() {
println!("Hello, world!");
}
pub fn say_goodbye() {
println!("Goodbye, world!");
}
}
fn main() {
// Now we can access these functions
greetings::say_hello();
greetings::say_goodbye();
}
Output:
Hello, world!
Goodbye, world!
Module Privacy Rules
Rust has a simple but powerful privacy system for modules:
- By default, everything is private
- Use the
pub
keyword to make items public - You can access:
- Public items from any parent or child module
- Private items only within the same module or child modules
Let's see this in action:
mod outer {
pub fn public_function() {
println!("This is a public function");
private_function(); // Can access private function
}
fn private_function() {
println!("This is a private function");
}
pub mod inner {
pub fn inner_public_function() {
println!("This is a public function in an inner module");
// Can access parent's private function
super::private_function();
}
fn inner_private_function() {
println!("This is a private function in an inner module");
}
}
}
fn main() {
// Can access public function
outer::public_function();
// Can access public function in public inner module
outer::inner::inner_public_function();
// These would fail - private functions
// outer::private_function();
// outer::inner::inner_private_function();
}
Output:
This is a public function
This is a private function
This is a public function in an inner module
This is a private function
Module Organization in Files
As your projects grow, you'll want to split your code across multiple files. Rust offers several ways to organize modules in files:
1. Module in the same file
We've already seen this approach:
// In main.rs
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
println!("2 + 3 = {}", math::add(2, 3));
}
2. Module in a separate file
For larger modules, place them in separate files:
File structure:
src/
main.rs
math.rs
// In math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
// In main.rs
// Tell Rust to load the "math" module from math.rs
mod math;
fn main() {
println!("2 + 3 = {}", math::add(2, 3));
println!("5 - 2 = {}", math::subtract(5, 2));
}
3. Module in a directory
For even larger modules with submodules:
File structure:
src/
main.rs
math/
mod.rs
advanced.rs
// In math/mod.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
// Include the advanced submodule
pub mod advanced;
// In math/advanced.rs
pub fn square(x: i32) -> i32 {
x * x
}
pub fn cube(x: i32) -> i32 {
x * x * x
}
// In main.rs
mod math;
fn main() {
println!("2 + 3 = {}", math::add(2, 3));
println!("5 - 2 = {}", math::subtract(5, 2));
println!("4² = {}", math::advanced::square(4));
println!("3³ = {}", math::advanced::cube(3));
}
Output:
2 + 3 = 5
5 - 2 = 3
4² = 16
3³ = 27
The use
Keyword
Typing full paths can become tedious. The use
keyword allows you to bring items into scope:
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub mod advanced {
pub fn square(x: i32) -> i32 {
x * x
}
}
}
// Bring functions into scope
use math::add;
use math::advanced::square;
fn main() {
// Now we can call them directly
println!("2 + 3 = {}", add(2, 3));
println!("4² = {}", square(4));
// We can still use the full path if needed
println!("3 + 4 = {}", math::add(3, 4));
}
Output:
2 + 3 = 5
4² = 16
3 + 4 = 7
Practical Example: Building a Calculator
Let's build a simple calculator application to demonstrate how modules can organize real-world code:
// Define our calculator module
mod calculator {
// Basic operations
pub mod basic {
pub fn add(a: f64, b: f64) -> f64 {
a + b
}
pub fn subtract(a: f64, b: f64) -> f64 {
a - b
}
pub fn multiply(a: f64, b: f64) -> f64 {
a * b
}
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("Division by zero".to_string())
} else {
Ok(a / b)
}
}
}
// Scientific operations
pub mod scientific {
pub fn square(x: f64) -> f64 {
x * x
}
pub fn square_root(x: f64) -> Result<f64, String> {
if x < 0.0 {
Err("Cannot take square root of negative number".to_string())
} else {
Ok(x.sqrt())
}
}
pub fn power(base: f64, exponent: i32) -> f64 {
base.powi(exponent)
}
}
// Memory operations
pub mod memory {
use std::cell::RefCell;
thread_local! {
static MEMORY: RefCell<f64> = RefCell::new(0.0);
}
pub fn store(value: f64) {
MEMORY.with(|m| *m.borrow_mut() = value);
}
pub fn recall() -> f64 {
MEMORY.with(|m| *m.borrow())
}
pub fn clear() {
MEMORY.with(|m| *m.borrow_mut() = 0.0);
}
}
}
// Import the parts we need
use calculator::basic::{add, subtract, multiply, divide};
use calculator::scientific::{square, square_root};
use calculator::memory::{store, recall};
fn main() {
// Basic operations
println!("3 + 4 = {}", add(3.0, 4.0));
println!("7 - 2 = {}", subtract(7.0, 2.0));
println!("5 × 6 = {}", multiply(5.0, 6.0));
// Handle potential errors
match divide(10.0, 2.0) {
Ok(result) => println!("10 ÷ 2 = {}", result),
Err(e) => println!("Error: {}", e),
}
match divide(10.0, 0.0) {
Ok(result) => println!("10 ÷ 0 = {}", result),
Err(e) => println!("Error: {}", e),
}
// Scientific operations
println!("5² = {}", square(5.0));
match square_root(16.0) {
Ok(result) => println!("√16 = {}", result),
Err(e) => println!("Error: {}", e),
}
// Memory operations
let result = add(5.0, 7.0);
println!("5 + 7 = {}", result);
store(result);
println!("Stored in memory: {}", recall());
let new_result = multiply(recall(), 2.0);
println!("Memory × 2 = {}", new_result);
}
Output:
3 + 4 = 7
7 - 2 = 5
5 × 6 = 30
10 ÷ 2 = 5
Error: Division by zero
5² = 25
√16 = 4
5 + 7 = 12
Stored in memory: 12
Memory × 2 = 24
This example shows how modules allow you to:
- Organize related functionality (basic, scientific, memory operations)
- Control visibility (all functions are public for this example)
- Create a clear and maintainable structure
Common Issues and Solutions
Issue 1: "Not found in this scope"
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
// This will cause an error
let sum = add(2, 3);
}
Solution: Either use the full path or bring it into scope:
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
// Option 1: Use full path
let sum1 = math::add(2, 3);
// Option 2: Bring into scope
use math::add;
let sum2 = add(4, 5);
}
Issue 2: "Private module"
mod outer {
mod inner {
pub fn inner_function() {
println!("Inner function");
}
}
}
fn main() {
// This will cause an error
outer::inner::inner_function();
}
Solution: Make sure all modules in the path are public:
mod outer {
pub mod inner {
pub fn inner_function() {
println!("Inner function");
}
}
}
fn main() {
// Now this works
outer::inner::inner_function();
}
Summary
In this tutorial, we've covered the basics of Rust's module system:
- Creating modules using the
mod
keyword - Privacy controls with the
pub
keyword - Organizing modules across files and directories
- Importing items with the
use
keyword - Building modular code with a real-world calculator example
Modules are a powerful tool in Rust for organizing your code as your projects grow. They help you create a logical structure, control access to your code's components, and prevent naming conflicts.
Exercises
-
Basic Module Structure
- Create a module called
geometry
with functions to calculate the area of various shapes (circle, rectangle, triangle) - Make the functions accessible from outside the module
- Use the module in a main function to calculate and print different areas
- Create a module called
-
Module Hierarchy
- Create a module called
utils
with submodules forstring
,math
, andfile
operations - Implement at least two functions in each submodule
- Create a main function that uses functions from each submodule
- Create a module called
-
File Organization
- Create a project with a module structure spread across files
- Implement a simple "to-do list" application with modules for tasks, users, and storage
- Practice using the
use
statement to simplify your code
-
Module Privacy
- Create a module with both public and private functions
- Implement helper functions that are only used within the module
- Demonstrate how privacy controls can be used to hide implementation details
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)