Modules, Visibility and Testing · about 45 min

Modules, Visibility and Paths

Why this matters

As programs grow past a few hundred lines, one flat file of functions becomes hard to navigate, and anything can call anything. Modules group related code under a name and control what is visible from outside. Privacy is not just tidiness: a module that keeps its fields private can guarantee invariants (a temperature never below absolute zero, a sorted list that stays sorted), because no outside code can break them. This lesson uses inline modules in a single file, which is what the exercises support, and then shows how the same structure maps onto files in a cargo project.

Defining modules

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

    pub mod stats {
        pub fn mean(v: &[f64]) -> f64 {
            v.iter().sum::<f64>() / v.len() as f64
        }
    }

    fn secret() -> i32 {
        42
    }

    pub fn reveal() -> i32 {
        secret()
    }
}

fn main() {
    println!("{}", math::add(2, 3));
    println!("{}", math::stats::mean(&[1.0, 2.0, 6.0]));
    println!("{}", math::reveal());
}
5
3
42

Using a private item from outside:

mod math {
    fn secret() -> i32 {
        42
    }
}

fn main() {
    println!("{}", math::secret());
}
error[E0603]: function `secret` is private
 --> main.rs:8:26
  |
8 |     println!("{}", math::secret());
  |                          ^^^^^^ private function
  |
note: the function `secret` is defined here
 --> main.rs:2:5
  |
2 |     fn secret() -> i32 {
  |     ^^^^^^^^^^^^^^^^^^

E0603 points at the private item. The rule of thumb is privacy flows down, not up: children can see their ancestors' private items, parents cannot see their children's.

Structs: public type, private fields

pub on a struct makes the type public, but each field stays private unless marked pub too:

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

    impl Account {
        pub fn new(owner: &str) -> Account {
            Account { owner: owner.to_string(), balance: 0 }
        }
        pub fn deposit(&mut self, amount: i64) {
            if amount > 0 {
                self.balance += amount;
            }
        }
        pub fn balance(&self) -> i64 {
            self.balance
        }
    }
}

fn main() {
    let mut a = bank::Account::new("Ada");
    a.deposit(100);
    a.deposit(-50);
    println!("{} has {}", a.owner, a.balance());
}
Ada has 100

Outside bank, nobody can write a.balance = -1_000_000 or construct an Account with a literal, so the only way to change the balance is through deposit, which enforces the rule. Trying:

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

fn main() {
    let a = bank::Account { owner: String::from("Eve"), balance: 1_000_000 };
}
error[E0451]: field `balance` of struct `Account` is private
 --> main.rs:9:57
  |
9 |     let a = bank::Account { owner: String::from("Eve"), balance: 1_000_000 };
  |                                                         ^^^^^^^^^^^^^^^^^^ private field

Enums are different: if an enum is pub, all its variants are public, since an enum with hidden variants could not be matched.

Paths: crate, self, super

Paths can be absolute (starting from the crate root with crate::) or relative (starting from the current module, or from the parent with super::):

mod shop {
    pub fn tax(amount: f64) -> f64 {
        amount * 0.08
    }

    pub mod cart {
        pub fn total(prices: &[f64]) -> f64 {
            let sum: f64 = prices.iter().sum();
            sum + super::tax(sum)
        }

        pub fn total_absolute(prices: &[f64]) -> f64 {
            let sum: f64 = prices.iter().sum();
            sum + crate::shop::tax(sum)
        }
    }
}

fn main() {
    println!("{:.2} {:.2}", shop::cart::total(&[10.0, 15.0]), shop::cart::total_absolute(&[10.0, 15.0]));
}
27.00 27.00

self:: means the current module (rarely needed). Prefer super:: for close relatives and crate:: for things far away; absolute paths keep working when you move a module around.

use: shortening paths

use brings a path into scope, so you can write the last part only:

mod shapes {
    pub mod circle {
        pub fn area(r: f64) -> f64 {
            3.14159 * r * r
        }
    }
    pub mod square {
        pub fn area(s: f64) -> f64 {
            s * s
        }
    }
}

use shapes::circle;
use shapes::square::area as square_area;
use std::collections::{BTreeMap, HashSet};

fn main() {
    println!("{:.2} {}", circle::area(1.0), square_area(3.0));
    let mut m = BTreeMap::new();
    m.insert("k", 1);
    let s: HashSet<i32> = [1, 2].into_iter().collect();
    println!("{:?} {}", m, s.len());
}
3.14 9
{"k": 1} 2

Other visibility levels

Modules in files (cargo projects)

In a real project, modules usually live in separate files. The module tree is the same; only where the code sits changes:

src/
  main.rs           // contains: mod geometry; mod parser;
  geometry.rs       // the body of mod geometry
  parser/
    mod.rs          // the body of mod parser (or src/parser.rs), contains: mod lexer;
    lexer.rs        // the body of mod parser::lexer

mod geometry; with a semicolon (no braces) tells the compiler "load this module's body from geometry.rs". The crate root is src/main.rs for a binary or src/lib.rs for a library; a package can have both, and the binary then uses the library as an external crate by its package name. A package (one Cargo.toml) contains one or more crates; each crate is a tree of modules. Everything you learned with inline modules applies unchanged.

Common mistakes

Recap

Going further: read the Rust Book chapter 7, "Managing Growing Projects with Packages, Crates, and Modules". Then create a cargo project in the sandbox shell and move a module from main.rs into its own file.

Exercises

Exercise 1 · write code

Make the right things public

This program does not compile: main uses items inside mod geometry that are private (E0603). Add pub where needed, but keep the helper square private (it is only used inside the module). The program must print:

area 12.00
distance 5.00

File: main.rs

Starting point

mod geometry {
    fn square(x: f64) -> f64 {
        x * x
    }

    fn rect_area(w: f64, h: f64) -> f64 {
        w * h
    }

    fn distance(x: f64, y: f64) -> f64 {
        (square(x) + square(y)).sqrt()
    }
}

fn main() {
    println!("area {:.2}", geometry::rect_area(3.0, 4.0));
    println!("distance {:.2}", geometry::distance(3.0, 4.0));
}

Example

expected output

area 12.00
distance 5.00
Hint 1

Put pub before fn rect_area and fn distance. Code inside the module can still call the private square.

Reference solution
mod geometry {
    fn square(x: f64) -> f64 {
        x * x
    }

    pub fn rect_area(w: f64, h: f64) -> f64 {
        w * h
    }

    pub fn distance(x: f64, y: f64) -> f64 {
        (square(x) + square(y)).sqrt()
    }
}

fn main() {
    println!("area {:.2}", geometry::rect_area(3.0, 4.0));
    println!("distance {:.2}", geometry::distance(3.0, 4.0));
}

Exercise 2 · write code

Enforce an invariant with privacy

Inside mod temperature, define pub struct Celsius with a private field value: f64, and:

Because the field is private, code outside the module cannot create an invalid temperature. The given main reads numbers from stdin and prints:

20 C = 68.0 F
invalid: -300
-273.15 C = -459.7 F

for the lines 20, -300, -273.15.

File: main.rs

Starting point

use std::io;

fn main() {
    for line in io::stdin().lines() {
        let v: f64 = line.unwrap().trim().parse().unwrap();
        match temperature::Celsius::new(v) {
            Some(c) => println!("{} C = {:.1} F", c.value(), c.to_fahrenheit()),
            None => println!("invalid: {v}"),
        }
    }
}

Example

input

20
-300
-273.15

expected output

20 C = 68.0 F
invalid: -300
-273.15 C = -459.7 F
Hint 1

pub struct Celsius { value: f64 } makes the type public but leaves the field private.

Hint 2

Methods in impl Celsius need pub individually to be callable from main.

Reference solution
use std::io;

mod temperature {
    pub struct Celsius {
        value: f64,
    }

    impl Celsius {
        pub fn new(value: f64) -> Option<Celsius> {
            if value < -273.15 {
                None
            } else {
                Some(Celsius { value })
            }
        }

        pub fn value(&self) -> f64 {
            self.value
        }

        pub fn to_fahrenheit(&self) -> f64 {
            self.value * 9.0 / 5.0 + 32.0
        }
    }
}

fn main() {
    for line in io::stdin().lines() {
        let v: f64 = line.unwrap().trim().parse().unwrap();
        match temperature::Celsius::new(v) {
            Some(c) => println!("{} C = {:.1} F", c.value(), c.to_fahrenheit()),
            None => println!("invalid: {v}"),
        }
    }
}

Exercise 3 · quiz

Paths

Inside mod shop { mod cart { fn total() { ... } } fn tax() { ... } }, how can code in total call tax? Select all that apply.

Answer
  • super::tax()

  • crate::shop::tax()

super is the parent module (shop), and crate:: starts from the crate root, so both name the same function. self::tax() and plain tax() look inside cart, where there is no tax. Child modules can see their ancestors' private items, which is why tax does not need pub here.