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,
}