Guide contents

Lesson 46 of 94

Modules & Visibility

So far every example has lived in one flat file. Real programs need structure: a way to group related code, hide implementation details, and expose a small public API. Rust does it with modules, use, and pub.

Three things to know up front:

  • A Rust module is not tied to a directory. You can declare a module inline, in one file, or in a directory, and you always declare it explicitly with mod.
  • Everything is private by default, and "private" means private to the module, not to the file or the class.
  • Privacy is per module, not per type. A struct's private fields are visible to all code in the same module, including free functions. There is no "private to this class".
Concept Rust
module path crate::billing
import use crate::billing::Invoice;
public pub
crate-wide visibility pub(crate)
private (no modifier) scoped to the module
group of related free functions a module with free functions

The examples fit in one file by declaring modules inline with mod name { ... }. Near the end we'll see how the same code is split across files, which changes nothing about how paths and visibility work.

1. Your first module

mod declares a module. Items inside it are reached with a path, where :: separates the parts.

Rust
mod greetings {
    pub fn hello(name: &str) -> String {
        format!("Hello, {name}!")
    }
}

fn main() {
    println!("{}", greetings::hello("Ana"));
}
Output
Hello, Ana!

Without pub, hello would be private to greetings and main couldn't call it.

2. Private by default

Every item (function, struct, enum, constant, module) is private unless marked pub. Calling a private function from outside its module is a compile error.

Rust✗ does not compile · E0603
mod billing {
    pub fn total(prices: &[u32]) -> u32 {
        prices.iter().sum::<u32>() + tax(prices)
    }

    fn tax(prices: &[u32]) -> u32 {
        prices.iter().sum::<u32>() / 10
    }
}

fn main() {
    println!("{}", billing::total(&[100, 200]));
    println!("{}", billing::tax(&[100, 200]));
}
Output
error[E0603]: function `tax` is private
  --> src/main.rs:13:29
   |
13 |     println!("{}", billing::tax(&[100, 200]));
   |                             ^^^ private function
   |
note: the function `tax` is defined here
  --> src/main.rs:6:5
   |
 6 |     fn tax(prices: &[u32]) -> u32 {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Rust's default is private: only the module itself (and its child modules, as we'll see) can use tax.

3. Nested modules: every step of the path must be visible

Modules nest. To use outer::inner::f from outside, inner must be pub as well as f. A pub fn inside a private module is still unreachable from outside.

Rust✗ does not compile · E0603
mod shop {
    mod pricing {
        pub fn discount(price: u32) -> u32 {
            price * 90 / 100
        }
    }

    pub fn checkout(price: u32) -> u32 {
        pricing::discount(price) // fine: `shop` can see its own child
    }
}

fn main() {
    println!("{}", shop::checkout(200));
    println!("{}", shop::pricing::discount(200));
}
Output
error[E0603]: module `pricing` is private
  --> src/main.rs:15:26
   |
15 |     println!("{}", shop::pricing::discount(200));
   |                          ^^^^^^^  -------- function `discount` is not publicly re-exported
   |                          |
   |                          private module
   |
note: the module `pricing` is defined here
  --> src/main.rs:2:5
   |
 2 |     mod pricing {
   |     ^^^^^^^^^^^

Make the module public too, and the path works:

Rust
mod shop {
    pub mod pricing {
        pub fn discount(price: u32) -> u32 {
            price * 90 / 100
        }
    }
}

fn main() {
    println!("{}", shop::pricing::discount(200));
}
Output
180

4. Children see their parents' private items

Privacy flows downward: a module can use every item of its ancestors, private or not. It can't see into a sibling's or a child's private items.

Rust
mod app {
    const VERSION: &str = "2.4.1"; // private to `app`

    pub mod banner {
        pub fn show() {
            // A child module can reach the parent's private items via `super`.
            println!("MyApp v{}", super::VERSION);
        }
    }
}

fn main() {
    app::banner::show();
}
Output
MyApp v2.4.1

This is why unit tests live in a child module (mod tests) of the code they test: they can call private functions. The Testing lesson uses this a lot.

5. Siblings don't see each other's private items

orders and inventory are siblings under the crate root. orders can call inventory's public functions, but not its private ones.

Rust✗ does not compile · E0603
mod inventory {
    pub fn in_stock(sku: &str) -> bool {
        count(sku) > 0
    }

    fn count(sku: &str) -> u32 {
        if sku == "BOOK-1" { 3 } else { 0 }
    }
}

mod orders {
    pub fn place(sku: &str) {
        if crate::inventory::in_stock(sku) {
            println!("ordered {sku}, {} left", crate::inventory::count(sku) - 1);
        }
    }
}

fn main() {
    orders::place("BOOK-1");
}
Output
error[E0603]: function `count` is private
  --> src/main.rs:14:66
   |
14 |             println!("ordered {sku}, {} left", crate::inventory::count(sku) - 1);
   |                                                                  ^^^^^ private function
   |
note: the function `count` is defined here
  --> src/main.rs:6:5
   |
 6 |     fn count(sku: &str) -> u32 {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^

6. Paths: crate::, self::, and super::

Paths can be absolute (start at the crate root with crate::) or relative (start at the current module, self::, or its parent, super::). The crate root is the file with main (or lib.rs for a library).

Rust
mod network {
    pub fn connect() -> &'static str {
        "connected"
    }

    pub mod client {
        pub fn run() {
            println!("absolute: {}", crate::network::connect());
            println!("parent:   {}", super::connect());
            println!("self:     {}", self::helper());
            println!("plain:    {}", helper()); // same as self::helper()
        }

        fn helper() -> &'static str {
            "helper"
        }
    }
}

fn main() {
    network::client::run();
}
Output
absolute: connected
parent:   connected
self:     helper
plain:    helper

Prefer crate:: for items far away and super:: for items in the parent. Absolute paths keep working if you move the calling code; super:: keeps working if you move a parent and child together.

7. use brings a path into scope

Typing full paths gets tedious. use creates a shortcut. It only affects the module where it's written.

Rust
mod geometry {
    pub mod shapes {
        #[derive(Debug)]
        pub struct Rect {
            pub w: f64,
            pub h: f64,
        }

        pub fn area(r: &Rect) -> f64 {
            r.w * r.h
        }
    }
}

use geometry::shapes;
use geometry::shapes::Rect;

fn main() {
    let r = Rect { w: 3.0, h: 4.0 };
    println!("{r:?} has area {}", shapes::area(&r));
}
Output
Rect { w: 3.0, h: 4.0 } has area 12

The Rust convention:

  • For functions, import the parent module and call shapes::area(...). The prefix shows the function isn't local.
  • For types (structs, enums, traits), import the type itself: Rect, HashMap, Duration.

That matches how you already use the standard library: use std::collections::HashMap; and then HashMap::new(), but use std::mem; and then mem::swap(...).

8. Grouped imports, self, and as

Several imports from one parent can share a line with braces. self inside the braces imports the parent module itself. as renames an import, which solves name clashes.

Rust
use std::collections::{self, BTreeMap, HashMap};
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;

fn describe(f: &mut std::fmt::Formatter) -> FmtResult {
    write!(f, "custom")
}

fn read_config() -> IoResult<String> {
    Ok(String::from("port=8080"))
}

fn main() {
    let mut a: HashMap<&str, i32> = HashMap::new();
    let mut b: BTreeMap<&str, i32> = BTreeMap::new();
    let c: collections::HashSet<i32> = collections::HashSet::from([1]);
    a.insert("x", 1);
    b.insert("y", 2);
    let _ = describe;
    println!("{a:?} {b:?} {c:?} {:?}", read_config());
}
Output
{"x": 1} {"y": 2} {1} Ok("port=8080")

9. use inside a function

use can appear in any scope, including a single function. This keeps a rarely used import next to the code that needs it.

Rust
fn top_scores(scores: &mut Vec<u32>, n: usize) -> &[u32] {
    use std::cmp::Reverse;
    scores.sort_by_key(|&s| Reverse(s));
    &scores[..n.min(scores.len())]
}

fn main() {
    let mut scores = vec![72, 95, 88, 61, 99];
    println!("{:?}", top_scores(&mut scores, 3));
}
Output
[99, 95, 88]

10. Glob imports and use super::*

use path::* imports every public item. Avoid it in normal code because it hides where names come from. Two places where it's idiomatic: use super::*; in a test module, and a crate's prelude (next example).

Rust
mod temperature {
    pub fn c_to_f(c: f64) -> f64 {
        c * 9.0 / 5.0 + 32.0
    }

    pub fn f_to_c(f: f64) -> f64 {
        (f - 32.0) * 5.0 / 9.0
    }

    pub mod checks {
        use super::*; // everything from `temperature`, private items included

        pub fn round_trip(c: f64) -> bool {
            (f_to_c(c_to_f(c)) - c).abs() < 1e-9
        }
    }
}

use temperature::*;

fn main() {
    println!("{:.1} F", c_to_f(21.5));
    println!("round trip ok: {}", checks::round_trip(-40.0));
}
Output
70.7 F
round trip ok: true

11. A prelude module

Libraries often offer a prelude module that re-exports the items almost every user needs, so users write one glob import. The standard library does this for you automatically: Vec, String, Option, Some, Ok, Clone, and friends come from std::prelude.

Rust
mod shop {
    pub mod model {
        #[derive(Debug)]
        pub struct Product {
            pub name: String,
            pub cents: u64,
        }
    }

    pub mod money {
        pub fn format_cents(cents: u64) -> String {
            format!("${}.{:02}", cents / 100, cents % 100)
        }
    }

    pub mod prelude {
        pub use super::model::Product;
        pub use super::money::format_cents;
    }
}

use shop::prelude::*;

fn main() {
    let p = Product { name: String::from("Keyboard"), cents: 4999 };
    println!("{} costs {}", p.name, format_cents(p.cents));
}
Output
Keyboard costs $49.99

12. Struct fields have their own visibility

Making a struct pub does not make its fields public. Each field needs its own pub. Code outside the module can't read, write, or even name a private field, and so it can't construct the struct with a literal.

Rust✗ does not compile · E0451
mod users {
    pub struct User {
        pub name: String,
        password_hash: String,
    }
}

fn main() {
    let u = users::User { name: String::from("ana"), password_hash: String::from("x") };
    println!("{}", u.name);
}
Output
error[E0451]: field `password_hash` of struct `User` is private
 --> src/main.rs:9:54
  |
9 |     let u = users::User { name: String::from("ana"), password_hash: String::from("x") };
  |                                                      ^^^^^^^^^^^^^ private field

This is encapsulation (private fields plus public methods) applied at the module level.

13. Constructors enforce invariants

Private fields plus a public constructor let a module guarantee something about every value of a type. Outside code can only get an Email through Email::parse, so every Email in the program is valid.

Rust
mod contact {
    #[derive(Debug)]
    pub struct Email {
        address: String,
    }

    impl Email {
        pub fn parse(raw: &str) -> Result<Email, String> {
            let raw = raw.trim();
            match raw.split_once('@') {
                Some((user, domain)) if !user.is_empty() && domain.contains('.') => {
                    Ok(Email { address: raw.to_lowercase() })
                }
                _ => Err(format!("invalid email: {raw:?}")),
            }
        }

        pub fn domain(&self) -> &str {
            self.address.split_once('@').map(|(_, d)| d).unwrap_or("")
        }

        pub fn as_str(&self) -> &str {
            &self.address
        }
    }
}

use contact::Email;

fn main() {
    for raw in ["  Ana@Example.COM ", "bob@", "@x.io", "carol@mail.org"] {
        match Email::parse(raw) {
            Ok(e) => println!("ok: {} (domain {})", e.as_str(), e.domain()),
            Err(msg) => println!("{msg}"),
        }
    }
}
Output
ok: ana@example.com (domain example.com)
invalid email: "bob@"
invalid email: "@x.io"
ok: carol@mail.org (domain mail.org)

14. Tuple structs with private fields

A tuple struct's fields are private too unless marked pub. With a private field, even the constructor UserId(5) is unavailable outside the module.

Rust✗ does not compile · E0603
mod ids {
    #[derive(Debug, Clone, Copy)]
    pub struct UserId(u64);

    pub fn next() -> UserId {
        UserId(42)
    }
}

fn main() {
    let a = ids::next();
    let b = ids::UserId(7);
    println!("{a:?} {b:?}");
}
Output
error[E0603]: tuple struct constructor `UserId` is private
  --> src/main.rs:12:18
   |
 3 |     pub struct UserId(u64);
   |                       --- a constructor is private if any of the fields is private
...
12 |     let b = ids::UserId(7);
   |                  ^^^^^^ private tuple struct constructor
   |
note: the tuple struct constructor `UserId` is defined here
  --> src/main.rs:3:5
   |
 3 |     pub struct UserId(u64);
   |     ^^^^^^^^^^^^^^^^^^^^^^^
help: consider making the field publicly accessible
   |
 3 |     pub struct UserId(pub u64);
   |                       +++

A common pattern is pub struct Meters(pub f64); for a transparent wrapper, and a private field when the wrapper must stay valid.

15. Enum variants are always public

Unlike struct fields, an enum's variants and their fields are all public when the enum is. There's no way to hide one variant: a match outside the module must be able to see all of them.

Rust
mod http {
    #[derive(Debug)]
    pub enum Status {
        Ok,
        NotFound { path: String },
        Error(u16),
    }

    pub fn fetch(path: &str) -> Status {
        match path {
            "/" => Status::Ok,
            "/boom" => Status::Error(500),
            other => Status::NotFound { path: other.to_string() },
        }
    }
}

use http::Status;

fn main() {
    for path in ["/", "/missing", "/boom"] {
        let text = match http::fetch(path) {
            Status::Ok => String::from("200 OK"),
            Status::NotFound { path } => format!("404 {path}"),
            Status::Error(code) => format!("{code} error"),
        };
        println!("{text}");
    }
}
Output
200 OK
404 /missing
500 error

16. Methods have their own visibility

A pub type can have private methods. Private methods are helpers for the type's own module, and every function in the module can call them.

Rust✗ does not compile · E0624
mod cache {
    pub struct Cache {
        hits: u32,
    }

    impl Cache {
        pub fn new() -> Self {
            Cache { hits: 0 }
        }

        pub fn get(&mut self, key: &str) -> Option<&'static str> {
            self.record_hit();
            if key == "home" { Some("<html>home</html>") } else { None }
        }

        fn record_hit(&mut self) {
            self.hits += 1;
        }
    }
}

fn main() {
    let mut c = cache::Cache::new();
    c.get("home");
    c.record_hit();
}
Output
error[E0624]: method `record_hit` is private
  --> src/main.rs:25:7
   |
16 |         fn record_hit(&mut self) {
   |         ------------------------ private method defined here
...
25 |     c.record_hit();
   |       ^^^^^^^^^^ private method

17. Privacy is per module, not per type

In Rust, private means "only this module", so a free function next to a struct can touch its private fields. This is often handy: a helper doesn't have to be a method.

Rust
mod account {
    pub struct Account {
        owner: String,
        balance: i64,
    }

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

    // Not a method, but in the same module, so it can use the private fields.
    pub fn transfer(from: &mut Account, to: &mut Account, amount: i64) -> Result<(), String> {
        if from.balance < amount {
            return Err(format!("{} has only {}", from.owner, from.balance));
        }
        from.balance -= amount;
        to.balance += amount;
        Ok(())
    }

    pub fn deposit(acc: &mut Account, amount: i64) {
        acc.balance += amount;
    }

    pub fn report(acc: &Account) -> String {
        format!("{}: {}", acc.owner, acc.balance)
    }
}

fn main() {
    let mut a = account::Account::open("ana");
    let mut b = account::Account::open("ben");
    account::deposit(&mut a, 100);
    println!("{:?}", account::transfer(&mut a, &mut b, 30));
    println!("{:?}", account::transfer(&mut b, &mut a, 50));
    println!("{} | {}", account::report(&a), account::report(&b));
}
Output
Ok(())
Err("ben has only 30")
ana: 70 | ben: 30

If you want "private to this type", put the type in its own small module.

18. pub(crate), pub(super), and pub(in path)

Between private and fully public there are scoped levels:

  • pub(crate): visible anywhere in this crate, but not to other crates that depend on it.
  • pub(super): visible to the parent module.
  • pub(in crate::some::module): visible within that module (which must be an ancestor).
Rust
mod engine {
    pub(crate) fn version() -> &'static str {
        "1.0"
    }

    pub mod parser {
        pub(super) fn tokenize(src: &str) -> Vec<&str> {
            src.split_whitespace().collect()
        }

        pub(in crate::engine) fn token_count(src: &str) -> usize {
            tokenize(src).len()
        }
    }

    pub fn run(src: &str) {
        // Both are visible here: `engine` is the parent of `parser`.
        println!("tokens: {:?}", parser::tokenize(src));
        println!("count: {}", parser::token_count(src));
    }
}

fn main() {
    println!("engine {}", engine::version()); // pub(crate): fine anywhere in the crate
    engine::run("let x = 5");
    // engine::parser::tokenize("x"); // error: only visible to `engine`
}
Output
engine 1.0
tokens: ["let", "x", "=", "5"]
count: 4

19. pub(super) outside its scope is an error

The restricted levels are enforced like plain privacy. Here main lives at the crate root, which is not the parent of parser, so tokenize is out of reach.

Rust✗ does not compile · E0603
mod engine {
    pub mod parser {
        pub(super) fn tokenize(src: &str) -> Vec<&str> {
            src.split_whitespace().collect()
        }
    }
}

fn main() {
    println!("{:?}", engine::parser::tokenize("a b"));
}
Output
error[E0603]: function `tokenize` is private
  --> src/main.rs:10:38
   |
10 |     println!("{:?}", engine::parser::tokenize("a b"));
   |                                      ^^^^^^^^ private function
   |
note: the function `tokenize` is defined here
  --> src/main.rs:3:9
   |
 3 |         pub(super) fn tokenize(src: &str) -> Vec<&str> {
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

20. pub use: re-exporting a flat public API

pub use imports an item and re-exports it, so users can reach it through the new path. Libraries use it to keep a deep internal structure while offering a short public path.

Rust
mod payments {
    mod gateway {
        pub mod stripe {
            pub fn charge(cents: u64) -> String {
                format!("stripe charged {cents}")
            }
        }

        pub mod paypal {
            pub fn charge(cents: u64) -> String {
                format!("paypal charged {cents}")
            }
        }
    }

    mod model {
        #[derive(Debug)]
        pub struct Receipt {
            pub id: u32,
            pub text: String,
        }
    }

    // The public face of the module: users never see `gateway` or `model`.
    pub use gateway::paypal::charge as paypal_charge;
    pub use gateway::stripe::charge;
    pub use model::Receipt;
}

fn main() {
    let r = payments::Receipt { id: 1, text: payments::charge(1999) };
    println!("{r:?}");
    println!("{}", payments::paypal_charge(500));
}
Output
Receipt { id: 1, text: "stripe charged 1999" }
paypal charged 500

The standard library does this constantly: std::collections::HashMap is really defined in std::collections::hash::map, and re-exported.

21. Traits must be in scope to call their methods

A trait's methods are only callable where the trait is in scope. The most common surprise: write! into a String needs std::fmt::Write.

Rust✗ does not compile · E0599
fn main() {
    let mut report = String::new();
    write!(report, "{} items", 3).unwrap();
    println!("{report}");
}
Output
error[E0599]: cannot write into `String`
 --> src/main.rs:3:12
  |
3 |     write!(report, "{} items", 3).unwrap();
  |            ^^^^^^
  |
 --> /rustc/<version>/library/core/src/fmt/mod.rs:212:7
  |
  = note: the method is available for `String` here
note: must implement `io::Write`, `fmt::Write`, or have a `write_fmt` method
 --> src/main.rs:3:12
  |
3 |     write!(report, "{} items", 3).unwrap();
  |            ^^^^^^
  = help: items from traits can only be used if the trait is in scope
help: trait `Write` which provides `write_fmt` is implemented but not in scope; perhaps you want to import it
  |
1 + use std::fmt::Write;
  |

Import the trait (not the type) and it works:

Rust
use std::fmt::Write;

fn main() {
    let mut report = String::new();
    write!(report, "{} items", 3).unwrap();
    writeln!(report, ", total ${:.2}", 59.97).unwrap();
    print!("{report}");
}
Output
3 items, total $59.97

Methods from traits are only visible where the trait is imported. That's also why a crate prelude usually re-exports its traits.

22. The same trait method from two traits

Two traits in scope can both define a method with the same name. Import each one where you need it, or call with a path to pick one explicitly.

Rust
mod export {
    pub trait Csv {
        fn render(&self) -> String;
    }

    pub trait Json {
        fn render(&self) -> String;
    }

    pub struct Point {
        pub x: i32,
        pub y: i32,
    }

    impl Csv for Point {
        fn render(&self) -> String {
            format!("{},{}", self.x, self.y)
        }
    }

    impl Json for Point {
        fn render(&self) -> String {
            format!("{{\"x\":{},\"y\":{}}}", self.x, self.y)
        }
    }
}

use export::{Csv, Json, Point};

fn main() {
    let p = Point { x: 3, y: -1 };
    // p.render() would be ambiguous here: both traits are in scope.
    println!("{}", Csv::render(&p));
    println!("{}", Json::render(&p));
}
Output
3,-1
{"x":3,"y":-1}

23. Constants, statics, and types in modules

Modules hold more than functions: constants, statics, type aliases, and traits all live in modules and follow the same privacy rules.

Rust
mod limits {
    pub const MAX_UPLOAD_MB: u32 = 25;
    pub const ALLOWED_TYPES: [&str; 3] = ["png", "jpg", "pdf"];
    pub type Megabytes = u32;

    pub fn check(name: &str, size: Megabytes) -> Result<(), String> {
        let ext = name.rsplit('.').next().unwrap_or("");
        if !ALLOWED_TYPES.contains(&ext) {
            return Err(format!("{name}: type .{ext} not allowed"));
        }
        if size > MAX_UPLOAD_MB {
            return Err(format!("{name}: {size} MB exceeds {MAX_UPLOAD_MB} MB"));
        }
        Ok(())
    }
}

fn main() {
    for (name, size) in [("cat.png", 2), ("movie.mp4", 700), ("scan.pdf", 40)] {
        match limits::check(name, size) {
            Ok(()) => println!("{name}: accepted"),
            Err(e) => println!("{e}"),
        }
    }
}
Output
cat.png: accepted
movie.mp4: type .mp4 not allowed
scan.pdf: 40 MB exceeds 25 MB

24. Splitting modules into files

Everything so far used inline mod name { ... }. In a real project, mod name; (with a semicolon) tells the compiler to load the module body from a file. This layout:

Text
src/
├── main.rs          crate root
├── config.rs        mod config
└── store/
    ├── mod.rs       mod store  (or store.rs next to the store/ directory)
    ├── memory.rs    mod store::memory
    └── disk.rs      mod store::disk

is declared like this:

Rust
// src/main.rs
mod config; // loads src/config.rs
mod store; // loads src/store.rs or src/store/mod.rs

fn main() {
    let cfg = config::load();
    let s = store::open(&cfg);
    s.put("k", "v");
}
Rust
// src/store/mod.rs  (or src/store.rs)
mod disk; // loads src/store/disk.rs
mod memory; // loads src/store/memory.rs

pub use memory::MemoryStore;

pub fn open(cfg: &crate::config::Config) -> MemoryStore {
    MemoryStore::with_capacity(cfg.capacity)
}

Things to know:

  • A file is not a module until some parent declares it with mod. A .rs file nobody declares is simply not compiled.
  • The module path comes from the mod declarations, not from the directory alone. The file is only where the body is stored.
  • store.rs + store/ and store/mod.rs are two spellings of the same thing. Newer code tends to use store.rs so that you don't end up with many tabs named mod.rs.
  • One file can hold many types. There is no "one public type per file" rule.

25. Realistic: a layered application

A typical service split into domain, repository, and service layers. Only the service's API is public; the repository is hidden behind it, and Order can only be created through Order::new.

Rust
mod domain {
    #[derive(Debug, Clone)]
    pub struct Order {
        id: u32,
        item: String,
        qty: u32,
    }

    impl Order {
        pub fn new(id: u32, item: &str, qty: u32) -> Result<Order, String> {
            if qty == 0 {
                return Err(String::from("quantity must be positive"));
            }
            Ok(Order { id, item: item.to_string(), qty })
        }

        pub fn id(&self) -> u32 {
            self.id
        }

        pub fn summary(&self) -> String {
            format!("#{} {} x{}", self.id, self.item, self.qty)
        }
    }
}

mod repository {
    use crate::domain::Order;
    use std::collections::HashMap;

    #[derive(Default)]
    pub(crate) struct OrderRepo {
        rows: HashMap<u32, Order>,
    }

    impl OrderRepo {
        pub(crate) fn save(&mut self, order: Order) {
            self.rows.insert(order.id(), order);
        }

        pub(crate) fn find(&self, id: u32) -> Option<&Order> {
            self.rows.get(&id)
        }
    }
}

pub mod service {
    use crate::domain::Order;
    use crate::repository::OrderRepo;

    #[derive(Default)]
    pub struct OrderService {
        repo: OrderRepo,
        next_id: u32,
    }

    impl OrderService {
        pub fn place(&mut self, item: &str, qty: u32) -> Result<u32, String> {
            let id = self.next_id + 1;
            let order = Order::new(id, item, qty)?; // `?` returns early: the id is not used up
            self.repo.save(order);
            self.next_id = id;
            Ok(id)
        }

        pub fn describe(&self, id: u32) -> String {
            match self.repo.find(id) {
                Some(o) => o.summary(),
                None => format!("#{id} not found"),
            }
        }
    }
}

use service::OrderService;

fn main() {
    let mut svc = OrderService::default();
    let a = svc.place("book", 2).unwrap();
    println!("{:?}", svc.place("pen", 0));
    println!("{}", svc.describe(a));
    println!("{}", svc.describe(99));
}
Output
Err("quantity must be positive")
#1 book x2
#99 not found

26. Realistic: a unit-safe money module

The money module guarantees that amounts are never negative and never mix currencies. The fields are private, so outside code can't build a bad Money, and add is the only way to combine them.

Rust
mod money {
    use std::fmt;

    #[derive(Debug, Clone, Copy, PartialEq)]
    pub enum Currency {
        Eur,
        Usd,
    }

    #[derive(Debug, Clone, Copy, PartialEq)]
    pub struct Money {
        cents: u64,
        currency: Currency,
    }

    impl Money {
        pub fn new(amount: f64, currency: Currency) -> Option<Money> {
            if amount.is_finite() && amount >= 0.0 {
                Some(Money { cents: (amount * 100.0).round() as u64, currency })
            } else {
                None
            }
        }

        pub fn add(self, other: Money) -> Result<Money, String> {
            if self.currency != other.currency {
                return Err(format!("cannot add {:?} to {:?}", other.currency, self.currency));
            }
            Ok(Money { cents: self.cents + other.cents, currency: self.currency })
        }
    }

    impl fmt::Display for Money {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            let symbol = match self.currency {
                Currency::Eur => "€",
                Currency::Usd => "$",
            };
            write!(f, "{symbol}{}.{:02}", self.cents / 100, self.cents % 100)
        }
    }
}

use money::{Currency, Money};

fn main() {
    let a = Money::new(19.99, Currency::Usd).unwrap();
    let b = Money::new(5.01, Currency::Usd).unwrap();
    let c = Money::new(3.0, Currency::Eur).unwrap();
    println!("{}", a.add(b).unwrap());
    println!("{:?}", a.add(c));
    println!("{:?}", Money::new(-1.0, Currency::Eur));
}
Output
$25.00
Err("cannot add Eur to Usd")
None

27. Realistic: a plugin registry with a sealed trait

Sometimes you want a public trait that outsiders can use but not implement, so you can add methods later without breaking anyone. The sealed trait pattern does this with a private module: the trait requires a supertrait that nobody outside the crate can name.

Rust
mod formats {
    mod sealed {
        pub trait Sealed {}
    }

    // Public trait, but implementing it needs `sealed::Sealed`, which is unreachable outside `formats`.
    pub trait Format: sealed::Sealed {
        fn name(&self) -> &'static str;
        fn encode(&self, fields: &[(&str, &str)]) -> String;
    }

    pub struct Csv;
    pub struct Query;

    impl sealed::Sealed for Csv {}
    impl sealed::Sealed for Query {}

    impl Format for Csv {
        fn name(&self) -> &'static str {
            "csv"
        }
        fn encode(&self, fields: &[(&str, &str)]) -> String {
            fields.iter().map(|(_, v)| *v).collect::<Vec<_>>().join(",")
        }
    }

    impl Format for Query {
        fn name(&self) -> &'static str {
            "query"
        }
        fn encode(&self, fields: &[(&str, &str)]) -> String {
            fields.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join("&")
        }
    }

    pub fn all() -> Vec<Box<dyn Format>> {
        vec![Box::new(Csv), Box::new(Query)]
    }
}

fn main() {
    let fields = [("name", "ana"), ("lang", "rust")];
    for f in formats::all() {
        println!("{:>5}: {}", f.name(), f.encode(&fields));
    }
}
Output
  csv: ana,rust
query: name=ana&lang=rust

Outside formats, writing impl formats::Format for MyType fails because MyType can't implement the private Sealed trait.

Summary

  • mod name { ... } declares a module inline; mod name; loads it from name.rs or name/mod.rs. Files are only compiled when a mod declares them.
  • Everything is private to its module by default. Child modules see their ancestors' private items; siblings and parents don't see a child's private items.
  • pub opens an item up. pub(crate), pub(super), and pub(in path) open it to a limited scope.
  • Struct fields and methods have their own visibility; enum variants are public with the enum. Private fields plus a constructor let a module enforce invariants.
  • Paths start with crate:: (absolute), self::, or super:: (relative). use creates shortcuts; as renames; pub use re-exports.
  • Traits must be in scope to call their methods.

Next: Crates, Packages & Workspaces shows how modules fit into crates, how Cargo manages dependencies and features, and how to structure multi-crate projects.