Skip to content
Learn Rust
Theme

Modules and Visibility

Abstractions · Lesson 38

A module is a named group of items, with its own private part. Everything inside is hidden from the outside until you mark it pub, and a use line gives a long path a short name.

Coming from GoA Go package is both the unit of privacy and the unit of compilation. Rust splits the two: a module (mod) is the privacy unit, a crate the compilation unit. You declare a module with mod instead of putting files in a folder, and visibility is pub instead of a capital letter.

Coming from KotlinKotlin declarations are public unless you say private or internal; Rust is the other way round, private unless pub. A Rust module is not a class: private is about the module: two types in one module can read each other's private fields. Kotlin's internal (the module) is pub(crate), and a Rust crate is what Kotlin calls a module.

Coming from PythonA Rust module is declared with mod, not found by file name, and without pub the compiler refuses the access, where Python's _name is only a request. A file does not join the program by being imported: its parent has to say mod. Your "package" is closer to a Rust crate.

Coming from TypeScriptpub is export, and an item without it cannot be named from outside. A file is not a module until its parent declares it with mod. use is import for names only: it does not load or run a file.

Coming from JavaA Rust module is a namespace like a package, but it is also the privacy boundary, and pub is closest to public. The default is the opposite: Java's no-modifier default is package-wide, Rust's is private to the module and its children. What Java calls a module (module-info.java) is a Rust crate.

Coming from C#A Rust module is like a namespace, use is using, and the default is private, like a C# member. The unit of privacy is the module rather than the class. A crate is like an assembly, and pub(crate) is the nearest thing to internal.

Coming from CThere are no headers: mod names a file inside a tree and does not paste text, and a crate is the translation unit. Items are private by default like static functions, and pub is what you would put in the header.

Coming from C++A module is a namespace that also controls access: private items are hidden from the outside, with no class needed. use is using, and there are no headers or include guards, because mod names a node in a tree instead of pasting text.

The quick comparison

Rustsrc/main.rsRun ↗
mod shop {
const PRICE: u32 = 10;
// No `pub`: only code inside `shop` can call this.
fn discount_percent(qty: u32) -> u32 {
if qty >= 10 { 20 } else { 0 }
}
pub fn total(qty: u32) -> u32 {
let full = qty * PRICE;
full - full * discount_percent(qty) / 100
}
}
fn main() {
println!("3 items: {}", shop::total(3));
println!("12 items: {}", shop::total(12));
}
$ cargo run
3 items: 30
12 items: 96
package main
import "fmt"
const price = 10
// Lowercase: visible only inside this package.
func discountPercent(qty int) int {
if qty >= 10 {
return 20
}
return 0
}
// Uppercase: visible to other packages.
func Total(qty int) int {
full := qty * price
return full - full*discountPercent(qty)/100
}
func main() {
fmt.Println("3 items:", Total(3))
fmt.Println("12 items:", Total(12))
}
$ go run main.go
3 items: 30
12 items: 96
object Shop {
private const val PRICE = 10
// private: only code inside `Shop` can call this
private fun discountPercent(qty: Int): Int = if (qty >= 10) 20 else 0
fun total(qty: Int): Int {
val full = qty * PRICE
return full - full * discountPercent(qty) / 100
}
}
fun main() {
println("3 items: ${Shop.total(3)}")
println("12 items: ${Shop.total(12)}")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
3 items: 30
12 items: 96
PRICE = 10
# The leading underscore is a convention: "internal, please don't call".
def _discount_percent(qty: int) -> int:
return 20 if qty >= 10 else 0
def total(qty: int) -> int:
full = qty * PRICE
return full - full * _discount_percent(qty) // 100
print("3 items:", total(3))
print("12 items:", total(12))
$ python3 main.py
3 items: 30
12 items: 96
namespace Shop {
const PRICE = 10;
// Not exported: invisible outside the namespace.
function discountPercent(qty: number): number {
return qty >= 10 ? 20 : 0;
}
export function total(qty: number): number {
const full = qty * PRICE;
return full - (full * discountPercent(qty)) / 100;
}
}
console.log("3 items:", Shop.total(3));
console.log("12 items:", Shop.total(12));
$ tsc --strict --module nodenext --types node main.ts && node main.js
3 items: 30
12 items: 96
public class Main {
static class Shop {
private static final int PRICE = 10;
// private: hidden from other classes (and from other packages)
private static int discountPercent(int qty) {
return qty >= 10 ? 20 : 0;
}
public static int total(int qty) {
int full = qty * PRICE;
return full - full * discountPercent(qty) / 100;
}
}
public static void main(String[] args) {
System.out.println("3 items: " + Shop.total(3));
System.out.println("12 items: " + Shop.total(12));
}
}
$ javac Main.java && java Main
3 items: 30
12 items: 96
Console.WriteLine($"3 items: {Shop.Total(3)}");
Console.WriteLine($"12 items: {Shop.Total(12)}");
static class Shop
{
const int Price = 10;
// Members are private unless you say otherwise.
static int DiscountPercent(int qty) => qty >= 10 ? 20 : 0;
public static int Total(int qty)
{
int full = qty * Price;
return full - full * DiscountPercent(qty) / 100;
}
}
$ dotnet run
3 items: 30
12 items: 96
#include <stdio.h>
#define PRICE 10
/* static: this name is private to this file */
static int discount_percent(int qty) {
return qty >= 10 ? 20 : 0;
}
int total(int qty) {
int full = qty * PRICE;
return full - full * discount_percent(qty) / 100;
}
int main(void) {
printf("3 items: %d\n", total(3));
printf("12 items: %d\n", total(12));
}
$ gcc -Wall -Wextra main.c -o main && ./main
3 items: 30
12 items: 96
#include <iostream>
namespace shop {
namespace {
// Unnamed namespace: private to this file.
int discount_percent(int qty) {
return qty >= 10 ? 20 : 0;
}
} // namespace
constexpr int price = 10;
int total(int qty) {
int full = qty * price;
return full - full * discount_percent(qty) / 100;
}
} // namespace shop
int main() {
std::cout << "3 items: " << shop::total(3) << "\n";
std::cout << "12 items: " << shop::total(12) << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
3 items: 30
12 items: 96

What's different

  • shop::total(3) is a path: the module name, ::, then the item. discount_percent has no pub, so only code inside shop can call it, even though main is in the same file.
  • A module can be written inline, as here, or live in its own file: mod shop; loads shop.rs. The step-by-step guide to files is linked below.
  • pub is per item. Nothing is public because it is in a module, and a struct's fields are private unless each one is marked pub.
  • use shop::total; would let main write total(3). It only gives a name a shortcut; it never loads a file.
Gotcha

Private is the default

Looks fine: in Go, Python, Java, C and C++ it runs fine. Rust won't compile it.

Works in Go. In Rust it's just a rule.Kotlin refuses this too. Same idea, different words.Works in Python. In Rust it's just a rule.TypeScript refuses this too. Same idea, different words.Works in Java. In Rust it's just a rule.C# refuses this too. Same idea, different words.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
mod shop {
// No `pub`.
fn discount_percent(qty: u32) -> u32 {
if qty >= 10 { 20 } else { 0 }
}
pub fn total(qty: u32) -> u32 {
let full = qty * 10;
full - full * discount_percent(qty) / 100
}
}
fn main() {
println!("total: {}", shop::total(12));
println!("discount: {}%", shop::discount_percent(12));
}
package main
import "fmt"
func discountPercent(qty int) int {
if qty >= 10 {
return 20
}
return 0
}
func Total(qty int) int {
full := qty * 10
return full - full*discountPercent(qty)/100
}
func main() {
fmt.Println("total:", Total(12))
fmt.Printf("discount: %d%%\n", discountPercent(12))
}
$ go run main.go✓ runs
total: 96
discount: 20%
Kotlin✗ refuses too

Kotlin refuses this too: discountPercent is private in Shop. Kotlin is public by default, so here the private was written on purpose.

object Shop {
private fun discountPercent(qty: Int): Int = if (qty >= 10) 20 else 0
fun total(qty: Int): Int {
val full = qty * 10
return full - full * discountPercent(qty) / 100
}
}
fun main() {
println("total: ${Shop.total(12)}")
println("discount: ${Shop.discountPercent(12)}%")
}
Main.kt:12:31: error: cannot access 'fun discountPercent(qty: Int): Int': it is private in 'Shop'.
    println("discount: ${Shop.discountPercent(12)}%")
                              ^^^^^^^^^^^^^^^
def _discount_percent(qty: int) -> int:
return 20 if qty >= 10 else 0
def total(qty: int) -> int:
full = qty * 10
return full - full * _discount_percent(qty) // 100
print("total:", total(12))
print(f"discount: {_discount_percent(12)}%")
$ python3 main.py✓ runs
total: 96
discount: 20%
TypeScript✗ refuses too

TypeScript refuses this: a function in a namespace that is not exported cannot be named from outside it.

namespace Shop {
function discountPercent(qty: number): number {
return qty >= 10 ? 20 : 0;
}
export function total(qty: number): number {
const full = qty * 10;
return full - (full * discountPercent(qty)) / 100;
}
}
console.log("total:", Shop.total(12));
console.log(`discount: ${Shop.discountPercent(12)}%`);
main.ts(13,31): error TS2339: Property 'discountPercent' does not exist on type 'typeof Shop'.
public class Main {
static class Shop {
private static int discountPercent(int qty) {
return qty >= 10 ? 20 : 0;
}
public static int total(int qty) {
int full = qty * 10;
return full - full * discountPercent(qty) / 100;
}
}
public static void main(String[] args) {
System.out.println("total: " + Shop.total(12));
System.out.println("discount: " + Shop.discountPercent(12) + "%");
}
}
$ javac Main.java && java Main✓ runs
total: 96
discount: 20%
C#✗ refuses too

C# refuses this too (CS0122): a member with no modifier is private, so the call from outside Shop is inaccessible.

Console.WriteLine($"total: {Shop.Total(12)}");
Console.WriteLine($"discount: {Shop.DiscountPercent(12)}%");
static class Shop
{
// No modifier on a member means private.
static int DiscountPercent(int qty) => qty >= 10 ? 20 : 0;
public static int Total(int qty)
{
int full = qty * 10;
return full - full * DiscountPercent(qty) / 100;
}
}
Program.cs(2,37): error CS0122: 'Shop.DiscountPercent(int)' is inaccessible due to its protection level
#include <stdio.h>
static int discount_percent(int qty) {
return qty >= 10 ? 20 : 0;
}
int total(int qty) {
int full = qty * 10;
return full - full * discount_percent(qty) / 100;
}
int main(void) {
printf("total: %d\n", total(12));
printf("discount: %d%%\n", discount_percent(12));
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
total: 96
discount: 20%
#include <iostream>
namespace shop {
namespace {
int discount_percent(int qty) {
return qty >= 10 ? 20 : 0;
}
} // namespace
int total(int qty) {
int full = qty * 10;
return full - full * discount_percent(qty) / 100;
}
} // namespace shop
int main() {
std::cout << "total: " << shop::total(12) << "\n";
std::cout << "discount: " << shop::discount_percent(12) << "%\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
total: 96
discount: 20%
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0603?
error[E0603]: function `discount_percent` is private
  --> src/main.rs:15:37
   |
15 |     println!("discount: {}%", shop::discount_percent(12));
   |                                     ^^^^^^^^^^^^^^^^ private function
   |
note: the function `discount_percent` is defined here
  --> src/main.rs:3:5
   |
 3 |     fn discount_percent(qty: u32) -> u32 {
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
What happens in Go

Go runs this and prints discount: 20%. The unit of privacy is the package, and this file is one package, so a lowercase name is visible to all of it. Another package would be refused (shown on this page).

What happens in Kotlin

Kotlin refuses this too: discountPercent is private in Shop. Kotlin is public by default, so here the private was written on purpose.

What happens in Python

Python prints discount: 20%. _discount_percent is private only by convention; nothing stops the call.

What happens in TypeScript

TypeScript refuses this: a function in a namespace that is not exported cannot be named from outside it.

What happens in Java

Java runs this and prints discount: 20%. private means private to the top-level class, and Shop is nested inside Main, so Main can reach it. A separate top-level class would be refused.

What happens in C#

C# refuses this too (CS0122): a member with no modifier is private, so the call from outside Shop is inaccessible.

What happens in C

C prints discount: 20%. static hides a function from other files, but this call is in the same file.

What happens in C++

C++ prints discount: 20%. An unnamed namespace hides names from other files, but this call is in the same file, and namespaces have no access control of their own.

What Rust sees

discount_percent has no pub, so it belongs to shop alone. Its callers are the code inside shop; main is outside.

Why Rust says no

Everything private by default means each pub is a decision to promise something to the outside. Whatever you leave private can be renamed or deleted without breaking anyone.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
mod shop {
pub fn discount_percent(qty: u32) -> u32 {
if qty >= 10 { 20 } else { 0 }
}
pub fn total(qty: u32) -> u32 {
let full = qty * 10;
full - full * discount_percent(qty) / 100
}
}
fn main() {
println!("total: {}", shop::total(12));
println!("discount: {}%", shop::discount_percent(12));
}
$ cargo run
total: 96
discount: 20%

The toolkit

Outputs on this page come from Linux on 64-bit ARM with Rust 1.99.1, edition 2024 (the language version a project’s Cargo.toml selects). This page is the reference: Splitting code into files is the step-by-step guide, and Reading signatures shows the path forms and pub.

Three words, defined once. A module (mod) is a named group of items that hides whatever you do not mark pub: a namespace and also the privacy boundary. A crate is one unit of compilation, a program or a library, made of a tree of modules. A Cargo package is a Cargo.toml with one or more crates beside it. Your language uses “package” and “module” for other things; the blocks at the end say which is which.

Modules, inline and in files

The top of the module tree is the crate root: src/main.rs for a program, src/lib.rs for a library. You can write a module inline, as the first program on this page does with mod shop { ... }, or in its own file with mod shop;, which makes the compiler read shop.rs.

Inside a crate, mod is how a source file joins the program (include! can paste one in, rarely: case 11). use only gives a path a shorter name and loads nothing. This program builds eleven small projects with cargo check and prints each file’s mod lines and the verdict:

Rustsrc/main.rsRun ↗
use std::fs;
use std::path::Path;
use std::process::Command;
// Builds a small project from `files` with `cargo check` and prints each file's
// `mod` lines, then whether it compiled and, if not, what the compiler said.
fn case(n: u32, title: &str, files: &[(&str, &str)]) {
let dir = std::env::temp_dir().join(format!("modules-lookup-{n}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("src")).unwrap();
fs::write(
dir.join("Cargo.toml"),
"[package]\nname = \"lookup\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
println!("{n}. {title}");
for (path, text) in files {
let full: &Path = &dir.join(path);
fs::create_dir_all(full.parent().unwrap()).unwrap();
fs::write(full, text).unwrap();
let mods: Vec<&str> = text
.lines()
.filter(|l| l.contains("mod ") || l.contains("include!"))
.collect();
let mods = if mods.is_empty() {
"-".to_string()
} else {
mods.join(" ")
};
println!(" {path:<14} {mods}");
}
let out = Command::new("cargo")
.args(["check", "-q"])
.current_dir(&dir)
.output()
.unwrap();
if out.status.success() {
println!(" -> ok");
}
let text = String::from_utf8_lossy(&out.stderr);
for line in text.lines() {
if line.starts_with("error[") || line.contains("= help") {
println!(" -> {}", line.trim());
}
}
}
// One line per case: a title, then (path, contents) for each file.
#[rustfmt::skip]
fn main() {
let main = "mod a;\nfn main() {}\n";
case(1, "a file next to the parent", &[("src/main.rs", main), ("src/a.rs", "")]);
case(2, "a folder with mod.rs", &[("src/main.rs", main), ("src/a/mod.rs", "")]);
case(3, "both of those", &[("src/main.rs", main), ("src/a.rs", ""), ("src/a/mod.rs", "")]);
case(4, "neither", &[("src/main.rs", main)]);
case(5, "a.rs declares a child", &[("src/main.rs", main), ("src/a.rs", "mod b;\n"), ("src/a/b.rs", "")]);
case(6, "a/mod.rs declares a child", &[("src/main.rs", main), ("src/a/mod.rs", "mod b;\n"), ("src/a/b.rs", "")]);
case(7, "the child file is beside a.rs", &[("src/main.rs", main), ("src/a.rs", "mod b;\n"), ("src/b.rs", "")]);
case(8, "an inline module declares a file", &[("src/main.rs", "mod a { mod b; }\nfn main() {}\n"), ("src/a/b.rs", "")]);
case(9, "a file nobody declares (it is not Rust)", &[("src/main.rs", main), ("src/a.rs", ""), ("src/extra.rs", "this is not Rust\n")]);
case(10, "the same mod line twice", &[("src/main.rs", "mod a;\nmod a;\nfn main() {}\n"), ("src/a.rs", "")]);
case(11, "include! instead of mod", &[("src/main.rs", "include!(\"extra.rs\");\nfn main() { extra(); }\n"), ("src/extra.rs", "fn extra() {}\n")]);
}
$ cargo run
1. a file next to the parent
  src/main.rs    mod a;
  src/a.rs       -
  -> ok
2. a folder with mod.rs
  src/main.rs    mod a;
  src/a/mod.rs   -
  -> ok
3. both of those
  src/main.rs    mod a;
  src/a.rs       -
  src/a/mod.rs   -
  -> error[E0761]: file for module `a` found at both "src/a.rs" and "src/a/mod.rs"
  -> = help: delete or rename one of them to remove the ambiguity
4. neither
  src/main.rs    mod a;
  -> error[E0583]: file not found for module `a`
  -> = help: to create the module `a`, create file "src/a.rs" or "src/a/mod.rs"
5. a.rs declares a child
  src/main.rs    mod a;
  src/a.rs       mod b;
  src/a/b.rs     -
  -> ok
6. a/mod.rs declares a child
  src/main.rs    mod a;
  src/a/mod.rs   mod b;
  src/a/b.rs     -
  -> ok
7. the child file is beside a.rs
  src/main.rs    mod a;
  src/a.rs       mod b;
  src/b.rs       -
  -> error[E0583]: file not found for module `b`
  -> = help: to create the module `b`, create file "src/a/b.rs" or "src/a/b/mod.rs"
8. an inline module declares a file
  src/main.rs    mod a { mod b; }
  src/a/b.rs     -
  -> ok
9. a file nobody declares (it is not Rust)
  src/main.rs    mod a;
  src/a.rs       -
  src/extra.rs   -
  -> ok
10. the same mod line twice
  src/main.rs    mod a; mod a;
  src/a.rs       -
  -> error[E0428]: the name `a` is defined multiple times
11. include! instead of mod
  src/main.rs    include!("extra.rs");
  src/extra.rs   -
  -> ok
  • A mod a; in the root finds src/a.rs or src/a/mod.rs (cases 1 and 2). Having both is E0761 (3), having neither is E0583 (4).
  • A module declared inside a looks in a folder named after a: src/a/b.rs, whether a is a.rs or a/mod.rs (5, 6). A file beside a.rs is the wrong place (7).
  • An inline module nests the folder too (8).
  • A file no mod names is never compiled (9). It was not valid Rust and nothing complained.
  • mod a; twice is E0428 (10): mod names one node of a tree, it does not paste text.

The Book calls a.rs the most idiomatic style and mod.rs the older one, still supported; mixing them “might be confusing”. Either way, the module tree is written down in mod lines, not discovered from the folders.

Private by default

Everything is private unless marked. A private item can be used in the module that holds it and in every module inside that one, and nowhere else (the Reference says so). pub widens that, and the forms in the middle name how far:

Written Visible in
nothing (or pub(self)) the module that holds the item, and the modules inside it
pub(super) the parent module, and what is inside it
pub(in crate::outer) that ancestor module, and what is inside it
pub(crate) the whole crate
pub everywhere the path to it is visible, other crates included

This program checks the table instead of asserting it. The function f lives in outer::mid::a. For each row it compiles a small library with a call to f from each place and reports what the compiler said. child is a module inside a, sibling is mid::b, cousin is outer::cousin:

Rustsrc/main.rsRun ↗
use std::fs;
use std::path::Path;
use std::process::Command;
// Compiles a small library once for each visibility, with a call to `f` from each place,
// and reports which calls the compiler accepted. Each cell is the result of a real
// `rustc` run, not typed in.
const PLACES: [&str; 7] = [
"in a",
"child",
"parent",
"sibling",
"cousin",
"root",
"other crate",
];
// `f` lives in `outer::mid::a`. The calls are on the lines listed in CALL_LINES.
fn library(vis: &str) -> String {
format!(
"pub mod outer {{
pub mod mid {{
pub mod a {{
{vis}fn f() {{}}
fn inside() {{ f(); }}
pub mod child {{
fn probe() {{ super::f(); }}
}}
}}
fn parent() {{ a::f(); }}
pub mod b {{
fn probe() {{ super::a::f(); }}
}}
}}
pub mod cousin {{
fn probe() {{ super::mid::a::f(); }}
}}
}}
fn root() {{ outer::mid::a::f(); }}
"
)
}
const CALL_LINES: [usize; 6] = [5, 7, 10, 12, 16, 19];
fn rustc(dir: &Path, args: &[&str]) -> String {
let out = Command::new("rustc")
.current_dir(dir)
.args([
"--edition",
"2024",
"-A",
"warnings",
"--error-format=short",
])
.args(args)
.output()
.unwrap();
String::from_utf8_lossy(&out.stderr).into_owned()
}
// "ok", or the error code of the first error in the compiler's output.
fn verdict(errors: &str) -> String {
match errors.find("error[") {
Some(i) => errors[i + 6..i + 11].to_string(),
None => "ok".to_string(),
}
}
fn row(name: &str, cells: &[String]) {
let mut line = format!("{name:<22}");
for c in cells {
line += &format!(" {c:<8}");
}
println!("{}", line.trim_end());
}
fn main() {
let dir = std::env::temp_dir().join("modules-matrix");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
row("visibility", &PLACES.map(String::from));
for vis in [
"",
"pub(super) ",
"pub(in crate::outer) ",
"pub(crate) ",
"pub ",
] {
// Calls from inside the crate.
fs::write(dir.join("calls.rs"), library(vis)).unwrap();
let errors = rustc(&dir, &["--crate-type=lib", "--emit=metadata", "calls.rs"]);
let mut cells: Vec<String> = Vec::new();
for line in CALL_LINES {
let hit = errors
.lines()
.find(|l| l.contains(&format!("calls.rs:{line}:")));
cells.push(hit.map_or("ok".to_string(), verdict));
}
// A call from another crate needs a library that has no errors of its own.
let clean =
format!("pub mod outer {{ pub mod mid {{ pub mod a {{ {vis}fn f() {{}} }} }} }}\n");
fs::write(dir.join("demo.rs"), clean).unwrap();
rustc(&dir, &["--crate-type=lib", "--emit=metadata", "demo.rs"]);
fs::write(
dir.join("user.rs"),
"fn main() { demo::outer::mid::a::f(); }\n",
)
.unwrap();
let errors = rustc(
&dir,
&[
"--emit=metadata",
"--extern",
"demo=libdemo.rmeta",
"user.rs",
],
);
cells.push(verdict(&errors));
let name = if vis.is_empty() {
"(no modifier)"
} else {
vis.trim()
};
row(name, &cells);
}
}
$ cargo run
visibility             in a     child    parent   sibling  cousin   root     other crate
(no modifier)          ok       ok       E0603    E0603    E0603    E0603    E0603
pub(super)             ok       ok       ok       ok       E0603    E0603    E0603
pub(in crate::outer)   ok       ok       ok       ok       ok       E0603    E0603
pub(crate)             ok       ok       ok       ok       ok       ok       E0603
pub                    ok       ok       ok       ok       ok       ok       ok

The pub(super) and pub(in crate::outer) rows differ at cousin, which is inside outer but not inside mid. pub(crate) stops at the crate boundary.

pub is not enough if a module on the way is private: a public item is reachable from outside only if all its ancestor modules are too. shop::rules::bulk_discount needs both rules and the function to be visible:

Rustsrc/main.rs✗ won't compile
mod shop {
mod rules {
pub fn bulk_discount(qty: u32) -> u32 {
if qty >= 10 { 20 } else { 0 }
}
}
pub fn total(qty: u32) -> u32 {
qty * 10 - qty * 10 * rules::bulk_discount(qty) / 100
}
}
fn main() {
println!("total: {}", shop::total(12));
println!("discount: {}", shop::rules::bulk_discount(12));
}
$ cargo buildWhat is E0603?
error[E0603]: module `rules` is private
  --> src/main.rs:15:36
   |
15 |     println!("discount: {}", shop::rules::bulk_discount(12));
   |                                    ^^^^^  ------------- function `bulk_discount` is not publicly re-exported
   |                                    |
   |                                    private module
   |
note: the module `rules` is defined here
  --> src/main.rs:2:5
   |
 2 |     mod rules {
   |     ^^^^^^^^^

A rule of thumb: start private, widen to pub(crate) when a sibling needs it, and mark pub only the crate’s interface. A pub item is a promise to every caller; a private one can be renamed or removed.

Fields and variants

A struct’s fields are private by default, one by one. A private field and a constructor let a module keep a promise, here “no Account has a negative balance”:

Rustsrc/main.rsRun ↗
mod account {
pub struct Account {
pub currency: &'static str,
balance_cents: i64, // private: only code in `account` can touch it
}
impl Account {
// The only way to build an Account from outside, so the rule below holds.
pub fn new(cents: i64) -> Option<Account> {
if cents < 0 {
return None;
}
Some(Account {
currency: "EUR",
balance_cents: cents,
})
}
pub fn balance(&self) -> i64 {
self.balance_cents
}
pub fn withdraw(&mut self, cents: i64) -> bool {
if cents > self.balance_cents {
return false;
}
self.balance_cents -= cents;
true
}
}
}
fn main() {
println!("negative start: {}", account::Account::new(-5).is_none());
let mut acct = account::Account::new(500).unwrap();
println!("currency: {}", acct.currency);
println!("withdraw 200: {}", acct.withdraw(200));
println!("withdraw 900: {}", acct.withdraw(900));
println!("balance: {}", acct.balance());
}
$ cargo run
negative start: true
currency: EUR
withdraw 200: true
withdraw 900: false
balance: 300

Outside the module the compiler enforces the promise: reading the field and building the struct by hand are both refused. Inside, only the module’s own code can break it (the second exercise is about this).

Rustsrc/main.rs✗ won't compile
mod account {
pub struct Account {
pub currency: &'static str,
balance_cents: i64, // no `pub`
}
impl Account {
pub fn new(cents: i64) -> Account {
Account {
currency: "EUR",
balance_cents: cents,
}
}
pub fn balance(&self) -> i64 {
self.balance_cents
}
}
}
fn main() {
let acct = account::Account::new(500);
println!("currency: {}", acct.currency);
println!("balance: {}", acct.balance_cents);
}
$ cargo buildWhat is E0616?
error[E0616]: field `balance_cents` of struct `Account` is private
  --> src/main.rs:24:34
   |
24 |     println!("balance: {}", acct.balance_cents);
   |                                  ^^^^^^^^^^^^^ private field
Rustsrc/main.rs✗ won't compile
mod account {
pub struct Account {
pub currency: &'static str,
balance_cents: i64,
}
impl Account {
pub fn balance(&self) -> i64 {
self.balance_cents
}
}
}
fn main() {
// Skips any check a constructor would make.
let acct = account::Account {
currency: "EUR",
balance_cents: -999,
};
println!("{} {}", acct.currency, acct.balance());
}
$ cargo buildWhat is E0451?
error[E0451]: field `balance_cents` of struct `Account` is private
  --> src/main.rs:18:9
   |
16 |     let acct = account::Account {
   |                ---------------- in this type
17 |         currency: "EUR",
18 |         balance_cents: -999,
   |         ^^^^^^^^^^^^^ private field

A tuple struct with a private field has a private constructor too:

Rustsrc/main.rs✗ won't compile
mod percent {
pub struct Percent(u8); // the field has no `pub`
impl Percent {
pub fn new(n: u8) -> Option<Percent> {
if n <= 100 { Some(Percent(n)) } else { None }
}
pub fn get(&self) -> u8 {
self.0
}
}
}
fn main() {
let ok = percent::Percent::new(50).unwrap();
let bad = percent::Percent(250);
println!("{} {}", ok.get(), bad.get());
}
$ cargo buildWhat is E0603?
error[E0603]: tuple struct constructor `Percent` is private
  --> src/main.rs:17:24
   |
 2 |     pub struct Percent(u8); // the field has no `pub`
   |                        -- a constructor is private if any of the fields is private
...
17 |     let bad = percent::Percent(250);
   |                        ^^^^^^^ private tuple struct constructor
   |
note: the tuple struct constructor `Percent` is defined here
  --> src/main.rs:2:5
   |
 2 |     pub struct Percent(u8); // the field has no `pub`
   |     ^^^^^^^^^^^^^^^^^^^^^^^
help: consider making the field publicly accessible
   |
 2 |     pub struct Percent(pub u8); // the field has no `pub`
   |                        +++

Enum variants are different: they take the visibility of the enum. A pub enum has public variants, while its struct neighbour still needs pub on each field:

Rustsrc/main.rsRun ↗
mod light {
// The variants are not marked `pub`: they take the enum's visibility.
pub enum Color {
Red,
Green,
}
pub struct Lamp {
pub on: bool, // struct fields do not work that way: each needs its own `pub`
}
}
fn main() {
for c in [light::Color::Red, light::Color::Green] {
let name = match c {
light::Color::Red => "red",
light::Color::Green => "green",
};
println!("color: {name}");
}
println!("lamp on: {}", light::Lamp { on: true }.on);
}
$ cargo run
color: red
color: green
lamp on: true

Re-exports

pub use makes an item reachable under a second path. The item is the same item; a library uses this to keep its file layout private and present a flat interface.

Rustsrc/main.rsRun ↗
mod shapes {
pub mod circle {
pub struct Circle {
pub r: f64,
}
pub fn area(c: &Circle) -> f64 {
std::f64::consts::PI * c.r * c.r
}
}
// A second name for the same item, one level up.
pub use self::circle::Circle;
}
use shapes::Circle;
fn main() {
let a: shapes::circle::Circle = Circle { r: 2.0 };
let b: shapes::Circle = a; // the same type, reached by two paths
println!("area: {:.2}", shapes::circle::area(&b));
}
$ cargo run
area: 12.57

a is typed with the long path and built with the short name Circle; b is typed with the short path. Moving a into b compiles only because both paths name one type. The Reference notes that when implementation is private and api is re-exported, outside crates may use api::f but not implementation::api::f.

Globs and the prelude

use a::*; imports every name of a that the importing module is allowed to use: the pub ones from outside, and also the private ones when the glob sits inside a (use super::*; in the tests below). Names written in the module itself win over glob imports:

Rustsrc/main.rsRun ↗
mod colors {
pub const RED: u32 = 1;
pub const GREEN: u32 = 2;
}
use colors::*;
// An item written in this module beats a glob import of the same name.
const GREEN: u32 = 99;
fn main() {
println!("RED: {RED}");
println!("GREEN: {GREEN}");
println!("colors::GREEN: {}", colors::GREEN);
}
$ cargo run
RED: 1
GREEN: 99
colors::GREEN: 2

If two globs bring the same name, using that name is an error until you choose one:

Rustsrc/main.rs✗ won't compile
mod metric {
pub fn describe() -> &'static str {
"metres"
}
}
mod imperial {
pub fn describe() -> &'static str {
"feet"
}
}
use imperial::*;
use metric::*;
fn main() {
println!("describe: {}", describe());
}
$ cargo buildWhat is E0659?
error[E0659]: `describe` is ambiguous
  --> src/main.rs:17:30
   |
17 |     println!("describe: {}", describe());
   |                              ^^^^^^^^ ambiguous name
   |
   = note: ambiguous because of multiple glob imports of a name in the same module
note: `describe` could refer to the function imported here
  --> src/main.rs:13:5
   |
13 | use imperial::*;
   |     ^^^^^^^^^^^
   = help: consider adding an explicit import of `describe` to disambiguate
note: `describe` could also refer to the function imported here
  --> src/main.rs:14:5
   |
14 | use metric::*;
   |     ^^^^^^^^^
   = help: consider adding an explicit import of `describe` to disambiguate

The prelude is a short list of names every module gets without writing use. This program asks the compiler about each name:

Rustsrc/main.rsRun ↗
use std::fs;
use std::process::Command;
// For each name, compile `let _ = NAME;` and look at what the compiler says. Error
// E0425 ("cannot find value") means the name is not in scope without a `use`. E0423
// means a type or trait with that name is in scope (it just is not a value), and no
// error means a value of that name is in scope.
const NAMES: &str = "Vec String Box Option Result Some None Ok Err Clone Copy Drop Default \
Iterator IntoIterator From Into TryFrom TryInto AsRef ToString ToOwned FromIterator \
PartialEq Ord Send Sync Fn drop Future HashMap HashSet Rc Arc Display Ordering fmt io";
fn main() {
let mut source = String::new();
for name in NAMES.split_whitespace() {
source += &format!("fn probe_{name}() {{ let _ = {name}; }}\n");
}
let dir = std::env::temp_dir().join("modules-prelude");
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
fs::write(dir.join("probe.rs"), source).unwrap();
let out = Command::new("rustc")
.current_dir(&dir)
.args(["--edition", "2024", "--crate-type=lib", "--emit=metadata"])
.args(["-A", "warnings", "--error-format=short", "probe.rs"])
.output()
.unwrap();
let errors = String::from_utf8_lossy(&out.stderr);
let (mut free, mut needs_use) = (Vec::new(), Vec::new());
for name in NAMES.split_whitespace() {
if errors.contains(&format!("error[E0425]: cannot find value `{name}`")) {
needs_use.push(name);
} else {
free.push(name);
}
}
println!("usable without a `use`: {}", free.join(" "));
println!("need a `use`: {}", needs_use.join(" "));
}
$ cargo run
usable without a `use`: Vec String Box Option Result Some None Ok Err Clone Copy Drop Default Iterator IntoIterator From Into TryFrom TryInto AsRef ToString ToOwned FromIterator PartialEq Ord Send Sync Fn drop Future
need a `use`: HashMap HashSet Rc Arc Display Ordering fmt io

The std docs list these and more, and say which edition added TryFrom, TryInto and FromIterator (2021) and Future (2024). Collections, Rc, Arc, the formatting traits and everything in fmt and io need a use:

Rustsrc/main.rs✗ won't compile
fn main() {
let mut stock = HashMap::new();
stock.insert("apples", 3);
println!("stock: {stock:?}");
}
$ cargo buildWhat is E0433?
error[E0433]: cannot find type `HashMap` in this scope
 --> src/main.rs:2:21
  |
2 |     let mut stock = HashMap::new();
  |                     ^^^^^^^ use of undeclared type `HashMap`
  |
help: consider importing this struct
  |
1 + use std::collections::HashMap;

Packages and crates

A workspace groups several packages that share a Cargo.lock and a target/ folder (more than one program). A package can hold a library crate (src/lib.rs), a program crate named like the package (src/main.rs) and more in src/bin/. This program builds one with a dependency on a second package, then breaks it two ways:

Rustsrc/main.rsRun ↗
use std::fs;
use std::path::Path;
use std::process::Command;
fn write(root: &Path, path: &str, text: &str) {
let full = root.join(path);
fs::create_dir_all(full.parent().unwrap()).unwrap();
fs::write(full, text).unwrap();
}
// Runs a cargo command in `dir`; prints its output, or only the error lines.
fn cargo(dir: &Path, args: &[&str], errors_only: bool) {
println!("$ cargo {}", args.join(" "));
let out = Command::new("cargo")
.args(args)
.current_dir(dir)
.output()
.unwrap();
let text =
String::from_utf8_lossy(&out.stdout).into_owned() + &String::from_utf8_lossy(&out.stderr);
for line in text.lines() {
let is_error = line.starts_with("error") && !line.contains("could not compile");
if !errors_only || is_error {
println!("{line}");
}
}
}
const UTIL_TOML: &str = r#"[package]
name = "my-util"
version = "0.1.0"
edition = "2024"
"#;
const APP_TOML: &str = r#"[package]
name = "app"
version = "0.1.0"
edition = "2024"
[dependencies]
my-util = { path = "../my-util" }
"#;
const UTIL_LIB: &str = r#"pub fn shout(s: &str) -> String {
s.to_uppercase()
}
"#;
const APP_LIB: &str = r#"pub fn greet() -> String {
my_util::shout(&secret())
}
pub(crate) fn secret() -> String {
String::from("hello")
}
"#;
fn main() {
let root = std::env::temp_dir().join("modules-crates");
let _ = fs::remove_dir_all(&root);
let app = root.join("app");
write(&root, "my-util/Cargo.toml", UTIL_TOML);
write(&root, "my-util/src/lib.rs", UTIL_LIB);
write(&root, "app/Cargo.toml", APP_TOML);
write(&root, "app/src/lib.rs", APP_LIB);
let main_rs = |body: &str| format!("fn main() {{\n println!(\"{{}}\", {body});\n}}\n");
write(&root, "app/src/main.rs", &main_rs("app::greet()"));
write(&root, "app/src/bin/tool.rs", &main_rs("app::greet()"));
cargo(&app, &["build", "-q"], false);
let mut built: Vec<String> = fs::read_dir(app.join("target/debug"))
.unwrap()
.map(|e| e.unwrap())
.filter(|e| e.path().is_file())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| !n.starts_with('.') && !n.ends_with(".d"))
.collect();
built.sort();
println!("target/debug (files, not counting .d): {}", built.join(" "));
cargo(&app, &["run", "-q", "--bin", "app"], false);
cargo(&app, &["run", "-q", "--bin", "tool"], false);
// main.rs is a different crate from lib.rs, so `pub(crate)` stops at lib.rs.
write(&root, "app/src/main.rs", &main_rs("app::secret()"));
cargo(&app, &["build", "-q"], true);
// And `crate::` in main.rs means main.rs's own crate.
write(&root, "app/src/main.rs", &main_rs("crate::greet()"));
cargo(&app, &["build", "-q"], true);
}
$ cargo run
$ cargo build -q
target/debug (files, not counting .d): app libapp.rlib tool
$ cargo run -q --bin app
HELLO
$ cargo run -q --bin tool
HELLO
$ cargo build -q
error[E0603]: function `secret` is private
$ cargo build -q
error[E0425]: cannot find function `greet` in the crate root
  • One package, three crates: the compiled library libapp.rlib and the programs app and tool.
  • main.rs and lib.rs are different crates. main.rs names the library by the package name (app::greet()), crate:: in main.rs means main.rs itself, and pub(crate) stops at the end of lib.rs.
  • my-util is a dependency listed in [dependencies]; code names it my_util, hyphen turned into underscore, and needs no mod. For crates.io, see using crates.

Tests live in a child module

The convention is a tests module at the bottom of the file, compiled only when testing. Because it is a child module, it can see its parent’s private items, and use super::*; brings them in:

Rustsrc/main.rsRun ↗
use std::fs;
use std::path::Path;
use std::process::Command;
fn write(root: &Path, path: &str, text: &str) {
let full = root.join(path);
fs::create_dir_all(full.parent().unwrap()).unwrap();
fs::write(full, text).unwrap();
}
// Runs one cargo command. `all` prints the test report lines; otherwise only the
// error lines. Test times and build folder hashes are cut, because they change.
fn cargo(dir: &Path, args: &[&str], all: bool) {
println!("$ cargo {}", args.join(" "));
// Through `sh`, so the compiler's and the test runner's lines keep their order.
let out = Command::new("sh")
.args(["-c", &format!("cargo {} 2>&1", args.join(" "))])
.current_dir(dir)
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stdout);
for line in text.lines() {
let line = line.split("; finished in").next().unwrap();
let line = line.split(" (target/").next().unwrap();
let is_error = line.starts_with("error") && !line.contains("could not compile");
let is_report = line.starts_with("test ")
|| line.trim_start().starts_with("Running")
|| line.contains("Doc-tests");
if is_error || (all && is_report) {
println!("{}", line.trim());
}
}
println!("(exit status {})", out.status.code().unwrap());
}
const LIB: &str = r#"pub fn total(qty: u32) -> u32 {
qty * 10 - qty * 10 * discount(qty) / 100
}
fn discount(qty: u32) -> u32 {
if qty >= 10 { 20 } else { 0 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bulk_discount_starts_at_ten() {
assert_eq!(discount(9), 0);
assert_eq!(discount(10), 20);
}
}
"#;
const API_TEST: &str = r#"#[test]
fn twelve_items() {
assert_eq!(shop::total(12), 96);
}
"#;
fn main() {
let dir = std::env::temp_dir().join("modules-tests").join("shop");
let _ = fs::remove_dir_all(&dir);
let toml = "[package]\nname = \"shop\"\nversion = \"0.1.0\"\nedition = \"2024\"\n";
write(&dir, "Cargo.toml", toml);
write(&dir, "src/lib.rs", LIB);
write(&dir, "tests/api.rs", API_TEST);
cargo(&dir, &["test"], true);
// An integration test is a separate crate: it sees only what is `pub`.
let peek = API_TEST.replace("shop::total(12)", "shop::discount(12)");
write(&dir, "tests/api.rs", &peek);
cargo(&dir, &["test"], false);
// Code under #[cfg(test)] is not compiled for a normal build.
write(&dir, "tests/api.rs", API_TEST);
let broken = LIB.replace("assert_eq!(discount(9), 0);", "let _x: u32 = \"nine\";");
write(&dir, "src/lib.rs", &broken);
cargo(&dir, &["build"], false);
cargo(&dir, &["test"], false);
}
$ cargo run
$ cargo test
Running unittests src/lib.rs
test tests::bulk_discount_starts_at_ten ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Running tests/api.rs
test twelve_items ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests shop
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
(exit status 0)
$ cargo test
error[E0603]: function `discount` is private
(exit status 101)
$ cargo build
(exit status 0)
$ cargo test
error[E0308]: mismatched types
(exit status 101)

#[cfg(test)] says “compile this only for tests” (attributes). Files in the tests/ folder are separate crates, so they see only what is pub (E0603 above). Broken test code does not stop a normal cargo build (the last runs).

Going deeper

Privacy follows the module, not the type

Another type in the same module can read a private field: privacy is about where the code is, not which type it belongs to.

Rustsrc/main.rsRun ↗
mod bank {
pub struct Account {
balance: i64,
}
pub struct Auditor;
impl Account {
pub fn new(balance: i64) -> Account {
Account { balance }
}
}
impl Auditor {
// A different type, but the same module: it can read the private field.
pub fn peek(&self, a: &Account) -> i64 {
a.balance
}
}
pub fn open() -> (Account, Auditor) {
(Account::new(250), Auditor)
}
}
fn main() {
let (account, auditor) = bank::open();
println!("peek: {}", auditor.peek(&account));
}
$ cargo run
peek: 250

This differs from a class-based private. If you want a type to protect a field from its neighbours, give the type a module of its own.

A public signature should not name a private type

A pub function that returns a type nobody outside can name is usually a slip: callers can hold the value but not write its type. The lint private_interfaces (a warning you can turn into an error) catches it; this program sets it to deny. In a program no other crate can call in, so the compiler measures the function’s reach as pub(crate): there pub and pub(crate) come to the same thing, and the difference starts with a library.

Rustsrc/main.rs✗ won't compile
#![deny(private_interfaces)]
mod shop {
struct Cents(u32); // private
// A public function whose signature names a private type.
pub fn price() -> Cents {
Cents(250)
}
}
fn main() {
println!("started");
}
$ cargo build
error: type `Cents` is more private than the item `price`
 --> src/main.rs:7:5
  |
7 |     pub fn price() -> Cents {
  |     ^^^^^^^^^^^^^^^^^^^^^^^ function `price` is reachable at visibility `pub(crate)`
  |
note: but type `Cents` is only usable at visibility `pub(self)`
 --> src/main.rs:4:5
  |
4 |     struct Cents(u32); // private
  |     ^^^^^^^^^^^^
note: the lint level is defined here
 --> src/main.rs:1:9
  |
1 | #![deny(private_interfaces)]
  |         ^^^^^^^^^^^^^^^^^^

The fix: make the type as visible as the function, or the function as private as the type.

A crate is compiled as a whole

The compiler starts at the root and follows every mod. There are no headers and no include order, and two modules of one crate may use each other:

Rustsrc/main.rsRun ↗
mod even {
pub fn is_even(n: u32) -> bool {
n == 0 || crate::odd::is_odd(n - 1)
}
}
mod odd {
pub fn is_odd(n: u32) -> bool {
n != 0 && crate::even::is_even(n - 1)
}
}
fn main() {
println!("is_even(10): {}", even::is_even(10));
println!("is_odd(7): {}", odd::is_odd(7));
}
$ cargo run
is_even(10): true
is_odd(7): true

Packages may not, and so neither may their crates. Cargo refuses a dependency cycle between packages:

Rustsrc/main.rsRun ↗
use std::fs;
use std::path::Path;
use std::process::Command;
fn write(root: &Path, path: &str, text: &str) {
let full = root.join(path);
fs::create_dir_all(full.parent().unwrap()).unwrap();
fs::write(full, text).unwrap();
}
fn main() {
let root = std::env::temp_dir().join("modules-cargo-cycle");
let _ = fs::remove_dir_all(&root);
// Two packages, each listing the other as a dependency.
for (name, other) in [("a", "b"), ("b", "a")] {
let toml = format!(
"[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]\n{other} = {{ path = \"../{other}\" }}\n"
);
write(&root, &format!("{name}/Cargo.toml"), &toml);
write(&root, &format!("{name}/src/lib.rs"), "");
}
let out = Command::new("cargo")
.args(["check", "-q"])
.current_dir(root.join("a"))
.output()
.unwrap();
let text = String::from_utf8_lossy(&out.stderr);
for line in text.lines().filter(|l| l.starts_with("error")) {
// Leave out the temporary folder the package sits in.
println!(
"{}",
line.replace(&format!(" ({})", root.join("a").display()), "")
);
}
}
$ cargo run
error: cyclic package dependency: package `a v0.1.0` depends on itself. Cycle:

In your language

Practice

Climb the visibility ladder

Write store::report::line, which formats "tea: 3 EUR" for 250 cents in region "fr". It needs util::round, private to util, and currency, private to store. One needs a wider visibility and the other needs none. main imports round from two places, so it compiles only if util::round is no wider than it must be.

Starterexercises/ladder.rs
mod store {
// No `pub`: visible in `store` and in every module inside it.
fn currency(region: &str) -> &'static str {
match region {
"fr" => "EUR",
"us" => "USD",
_ => "GBP",
}
}
pub mod util {
// Cents rounded to whole units. Private to `util` for now.
fn round(cents: u32) -> u32 {
(cents + 50) / 100
}
}
pub mod report {
// "tea: 3 EUR" for 250 cents in "fr". Use `util::round` and `currency`.
// Calling `round` while it is private to `util` is E0603: read it first.
pub fn line(name: &str, cents: u32, region: &str) -> String {
todo!("call util::round and currency; one of them needs a wider visibility")
}
}
}
// `rival` also has a `round`. With both globs below, `round(250)` in `main` is
// ambiguous (E0659) when `store::util::round` is visible here. Pick the narrowest
// visibility that still lets `report` call it, and the ambiguity goes away.
mod rival {
pub fn round(cents: u32) -> u32 {
cents / 100
}
}
#[allow(unused_imports)]
use rival::*;
#[allow(unused_imports)]
use store::util::*;
fn main() {
assert_eq!(round(250), 2);
assert_eq!(store::report::line("tea", 250, "fr"), "tea: 3 EUR");
assert_eq!(store::report::line("pen", 99, "us"), "pen: 1 USD");
assert_eq!(store::report::line("ale", 1000, "uk"), "ale: 10 GBP");
println!("ok");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/ladder.solution.rsRun ↗
mod store {
// No `pub`: visible in `store` and in every module inside it.
fn currency(region: &str) -> &'static str {
match region {
"fr" => "EUR",
"us" => "USD",
_ => "GBP",
}
}
pub mod util {
// Cents rounded to whole units. Visible in `store`, not beyond.
pub(super) fn round(cents: u32) -> u32 {
(cents + 50) / 100
}
}
pub mod report {
// "tea: 3 EUR" for 250 cents in "fr". Use `util::round` and `currency`.
// Calling `round` while it is private to `util` is E0603: read it first.
pub fn line(name: &str, cents: u32, region: &str) -> String {
format!(
"{name}: {} {}",
super::util::round(cents),
super::currency(region)
)
}
}
}
// `rival` also has a `round`. With both globs below, `round(250)` in `main` is
// ambiguous (E0659) when `store::util::round` is visible here. Pick the narrowest
// visibility that still lets `report` call it, and the ambiguity goes away.
mod rival {
pub fn round(cents: u32) -> u32 {
cents / 100
}
}
#[allow(unused_imports)]
use rival::*;
#[allow(unused_imports)]
use store::util::*;
fn main() {
assert_eq!(round(250), 2);
assert_eq!(store::report::line("tea", 250, "fr"), "tea: 3 EUR");
assert_eq!(store::report::line("pen", 99, "us"), "pen: 1 USD");
assert_eq!(store::report::line("ale", 1000, "uk"), "ale: 10 GBP");
println!("ok");
}
$ cargo run
ok

A number that cannot be wrong

Finish Percent, a number from 0 to 100, and its child module parse. The field has no pub, yet parse could write Percent(n) itself, because a child sees its parent’s private items. Decide whether it should: the checks include "150%". In report, outside percent, try p.0 and read the error.

Starterexercises/percent.rs
mod percent {
/// A number from 0 to 100. The field has no `pub`, so code outside this
/// module can only get a `Percent` through the functions below.
pub struct Percent(u8);
impl Percent {
pub fn new(n: u8) -> Option<Percent> {
todo!("accept 0 to 100, refuse the rest")
}
pub fn get(&self) -> u8 {
todo!("the number inside")
}
pub fn of(&self, amount: u32) -> u32 {
todo!("that percent of amount, rounded down: 25% of 200 is 50")
}
/// The sum of two percentages. It must never go past 100.
pub fn add(&self, other: &Percent) -> Percent {
todo!("add, and stop at 100")
}
}
/// Reading text such as "25%". A child module sees the private field of
/// `Percent`, so it can write `Percent(n)` itself. Should it?
pub mod parse {
use super::Percent;
pub fn from_text(s: &str) -> Option<Percent> {
todo!("strip the '%', parse a u8, and make the Percent")
}
}
}
// Outside `percent`: the field is not visible here, only the functions.
mod report {
use crate::percent::Percent;
pub fn show(p: &Percent) -> String {
format!("{}%", p.get())
}
}
use percent::{Percent, parse};
fn main() {
assert!(Percent::new(101).is_none());
assert!(Percent::new(255).is_none());
assert_eq!(Percent::new(0).unwrap().get(), 0);
assert_eq!(Percent::new(100).unwrap().get(), 100);
assert_eq!(Percent::new(25).unwrap().of(200), 50);
// Round down after multiplying: 33% of 50 is 16.5.
assert_eq!(Percent::new(33).unwrap().of(50), 16);
let sum = Percent::new(30).unwrap().add(&Percent::new(20).unwrap());
assert_eq!(sum.get(), 50);
let capped = Percent::new(60).unwrap().add(&Percent::new(70).unwrap());
assert_eq!(capped.get(), 100);
// Text goes through the same gate as numbers.
assert_eq!(report::show(&parse::from_text("25%").unwrap()), "25%");
assert!(parse::from_text("100%").is_some());
assert!(parse::from_text("150%").is_none());
assert!(parse::from_text("25").is_none());
assert!(parse::from_text("x%").is_none());
println!("ok");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/percent.solution.rsRun ↗
mod percent {
/// A number from 0 to 100. The field has no `pub`, so code outside this
/// module can only get a `Percent` through the functions below.
pub struct Percent(u8);
impl Percent {
pub fn new(n: u8) -> Option<Percent> {
if n <= 100 { Some(Percent(n)) } else { None }
}
pub fn get(&self) -> u8 {
self.0
}
pub fn of(&self, amount: u32) -> u32 {
amount * self.0 as u32 / 100
}
/// The sum of two percentages. It must never go past 100.
pub fn add(&self, other: &Percent) -> Percent {
Percent((self.0 + other.0).min(100))
}
}
/// Reading text such as "25%". A child module sees the private field of
/// `Percent`, so it can write `Percent(n)` itself. Should it?
pub mod parse {
use super::Percent;
pub fn from_text(s: &str) -> Option<Percent> {
let n: u8 = s.strip_suffix('%')?.parse().ok()?;
Percent::new(n)
}
}
}
// Outside `percent`: the field is not visible here, only the functions.
mod report {
use crate::percent::Percent;
pub fn show(p: &Percent) -> String {
format!("{}%", p.get())
}
}
use percent::{Percent, parse};
fn main() {
assert!(Percent::new(101).is_none());
assert!(Percent::new(255).is_none());
assert_eq!(Percent::new(0).unwrap().get(), 0);
assert_eq!(Percent::new(100).unwrap().get(), 100);
assert_eq!(Percent::new(25).unwrap().of(200), 50);
// Round down after multiplying: 33% of 50 is 16.5.
assert_eq!(Percent::new(33).unwrap().of(50), 16);
let sum = Percent::new(30).unwrap().add(&Percent::new(20).unwrap());
assert_eq!(sum.get(), 50);
let capped = Percent::new(60).unwrap().add(&Percent::new(70).unwrap());
assert_eq!(capped.get(), 100);
// Text goes through the same gate as numbers.
assert_eq!(report::show(&parse::from_text("25%").unwrap()), "25%");
assert!(parse::from_text("100%").is_some());
assert!(parse::from_text("150%").is_none());
assert!(parse::from_text("25").is_none());
assert!(parse::from_text("x%").is_none());
println!("ok");
}
$ cargo run
ok

Read more