RustArena

Modules and visibility in Rust: mod, pub and use

How Rust modules work: mod blocks and files, private by default, pub and pub(crate), paths with crate:: and super::, use, and re-exports.

Modules organize code into named groups and control what's visible from outside. Every crate has a tree of modules rooted at src/main.rs (for a binary) or src/lib.rs (for a library).

Private by default

mod bank {
    pub struct Account {
        pub owner: String,
        balance: u64,          // private field
    }

    impl Account {
        pub fn new(owner: &str) -> Self { ... }
        pub fn balance(&self) -> u64 { self.balance }
        fn audit(&self) { ... }  // private method
    }
}

Everything inside a module is private to that module (and its child modules) unless marked pub. That includes struct fields: a pub struct with private fields can only be created through the functions you provide, which is how you protect rules like "balance never goes negative".

Using private items from outside is a compile error:

Visibility Visible to
(nothing) this module and its children
pub everyone who can see the module
pub(crate) anywhere in this crate, not to users of your library
pub(super) the parent module

Paths and use

Refer to items with paths: crate::bank::Account (absolute, from the crate root), super::helper (the parent module), self::inner (this module). use brings a path into scope so you can write the short name:

use std::collections::{HashMap, HashSet};
use crate::bank::Account;
use std::io::Result as IoResult;   // rename to avoid a clash

Conventions: import types, traits and macros by name (use std::collections::HashMap;), but call functions through their module (use std::fs; then fs::read_to_string(...)), so readers can see where a function comes from.

Modules in files

A mod bank; declaration (with a semicolon instead of a body) tells the compiler to load the module from a file:

src/
├── main.rs        // mod bank;  mod report;
├── bank.rs        // the bank module; may declare `pub mod ledger;`
├── bank/
│   └── ledger.rs  // bank::ledger
└── report.rs

Files are never compiled unless some module declares them with mod. The older bank/mod.rs layout also works.

Re-exports with pub use

pub use makes an item available at a different path. Libraries use it to offer a flat, stable API while keeping a deeper internal structure: pub use crate::bank::ledger::Entry; in lib.rs lets users write mycrate::Entry.

Crates and packages

A crate is one compilation unit (a library or a binary). A package is what Cargo.toml describes: one or more crates. Dependencies from crates.io are added under [dependencies] and used with their crate name: use serde::Deserialize;.

Common mistakes

Example

mod bank {
    pub mod ledger {
        #[derive(Debug)]
        pub struct Entry {
            pub amount: i64,
            pub note: String,
        }

        pub(crate) fn describe(e: &Entry) -> String {
            format!("{:+} ({})", e.amount, e.note)
        }
    }

    use self::ledger::Entry;

    pub struct Account {
        pub owner: String,
        balance: i64,
        history: Vec<Entry>,
    }

    impl Account {
        pub fn new(owner: &str) -> Self {
            Account { owner: owner.to_string(), balance: 0, history: Vec::new() }
        }

        pub fn deposit(&mut self, amount: i64, note: &str) -> Result<(), String> {
            check_amount(amount)?;
            self.balance += amount;
            self.history.push(Entry { amount, note: note.to_string() });
            Ok(())
        }

        pub fn balance(&self) -> i64 {
            self.balance
        }

        pub fn history(&self) -> &[Entry] {
            &self.history
        }
    }

    fn check_amount(amount: i64) -> Result<(), String> {
        if amount <= 0 {
            return Err(format!("amount must be positive, got {amount}"));
        }
        Ok(())
    }
}

pub use bank::ledger::Entry;
use bank::Account;

fn main() {
    let mut acct = Account::new("Ferris");
    acct.deposit(100, "salary").unwrap();
    acct.deposit(25, "refund").unwrap();
    if let Err(e) = acct.deposit(-5, "oops") {
        println!("rejected: {e}");
    }
    println!("{} has {}", acct.owner, acct.balance());
    for e in acct.history() {
        println!("  {}", bank::ledger::describe(e));
    }
    let first: &Entry = &acct.history()[0];
    println!("{first:?}");
}

Output:

rejected: amount must be positive, got -5
Ferris has 125
  +100 (salary)
  +25 (refund)
Entry { amount: 100, note: "salary" }

Practice it