Phase 4Code organization

#13 Modules and crates

mod, use, pub, crate

What is a module?

Modules let you organize your code into logical groups. They control the visibility (public vs private) of items like functions, structs, and constants. Every Rust file is implicitly a module.

A crate is the top-level unit of compilation. A crate can be a binary (main.rs) or a library (lib.rs). Modules live inside crates.

The mod keyword

You define a module with the mod keyword. Modules can be nested:

Inline modules
mod greetings {
    pub fn hello() {
        println!("Hello from greetings module!");
    }

    fn secret() {
        println!("This is private");
    }
}

fn main() {
    greetings::hello();   // OK: hello is pub
    // greetings::secret(); // ERROR: secret is private
}

Items inside a module are private by default. Only the module itself and its children can access private items.

Visibility (pub)

The pub keyword makes an item accessible from outside its module. You can apply it to functions, structs, fields, enums, and more:

Visibility rules
mod geometry {
    pub struct Circle {
        pub radius: f64,  // public field
        color: String,    // private field
    }

    impl Circle {
        pub fn new(radius: f64) -> Circle {
            Circle {
                radius,
                color: String::from("red"),
            }
        }

        pub fn area(&self) -> f64 {
            std::f64::consts::PI * self.radius * self.radius
        }
    }
}

fn main() {
    let c = geometry::Circle::new(5.0);
    println!("Area: {:.2}", c.area());
    println!("Radius: {}", c.radius);  // OK: pub field
    // println!("{}", c.color);         // ERROR: private field
}

The use keyword

Writing the full path every time is verbose. The use keyword brings items into scope:

Bringing items into scope
mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    pub fn multiply(a: i32, b: i32) -> i32 {
        a * b
    }
}

// Bring specific items into scope
use math::add;
use math::multiply;

// Or bring multiple items at once
// use math::{add, multiply};

fn main() {
    println!("{}", add(2, 3));
    println!("{}", multiply(4, 5));
}

You can also rename imports with as:

Renaming imports
use std::collections::HashMap as Map;

let mut m = Map::new();
m.insert("key", "value");

Splitting into files

For larger projects, you put each module in its own file. The file structure mirrors the module hierarchy:

Project structure
src/
  main.rs         // crate root
  database.rs     // mod database
  network/
    mod.rs        // mod network
    client.rs     // mod network::client
    server.rs     // mod network::server

In main.rs, you declare the modules:

main.rs
mod database;
mod network;

use database::connect;
use network::client::fetch;

fn main() {
    connect("myapp.db");
    fetch("https://example.com");
}
src/database.rs
pub fn connect(db_name: &str) {
    println!("Connected to database: {}", db_name);
}
src/network/mod.rs
pub mod client;
pub mod server;
src/network/client.rs
pub fn fetch(url: &str) {
    println!("Fetching: {}", url);
}

Summary

Quick reference
mod name { }        // Define an inline module
pub fn/struct/...   // Make an item public
use path::item;     // Bring item into scope
use path::{a, b};   // Bring multiple items
use path::item as x; // Rename an import
mod name;           // Load module from file

// File-based modules:
// src/foo.rs         -> mod foo
// src/foo/mod.rs     -> mod foo (with submodules)
// src/foo/bar.rs     -> mod foo::bar

Your turn

Try building and running the modules demo with cargo build and cargo run:

terminal — cargo
user@stemlegacy:~/modules-demo$