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
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));}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))}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)}")}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))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));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)); }}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; }}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));}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";}3 items: 30 12 items: 96
What's different
shop::total(3)is a path: the module name,::, then the item.discount_percenthas nopub, so only code insideshopcan call it, even thoughmainis in the same file.- A module can be written inline, as here, or live in its own file:
mod shop;loadsshop.rs. The step-by-step guide to files is linked below. pubis per item. Nothing is public because it is in a module, and a struct's fields are private unless each one is markedpub.use shop::total;would letmainwritetotal(3). It only gives a name a shortcut; it never loads a file.
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.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))}total: 96 discount: 20%
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)}%")total: 96 discount: 20%
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) + "%"); }}total: 96 discount: 20%
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));}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";}total: 96 discount: 20%
What does the compiler say? Guess first, then open.
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 { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
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).
Kotlin refuses this too: discountPercent is private in Shop. Kotlin is public by default, so here the private was written on purpose.
Python prints discount: 20%. _discount_percent is private only by convention; nothing stops the call.
TypeScript refuses this: a function in a namespace that is not exported cannot be named from outside it.
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.
C# refuses this too (CS0122): a member with no modifier is private, so the call from outside Shop is inaccessible.
C prints discount: 20%. static hides a function from other files, but this call is in the same file.
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.
discount_percent has no pub, so it belongs to shop alone. Its callers are the code inside shop; main is outside.
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.
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));}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:
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")]);}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 findssrc/a.rsorsrc/a/mod.rs(cases 1 and 2). Having both is E0761 (3), having neither is E0583 (4). - A module declared inside
alooks in a folder named aftera:src/a/b.rs, whetheraisa.rsora/mod.rs(5, 6). A file besidea.rsis the wrong place (7). - An inline module nests the folder too (8).
- A file no
modnames is never compiled (9). It was not valid Rust and nothing complained. mod a;twice is E0428 (10):modnames 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:
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); }}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:
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));}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”:
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());}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).
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);}error[E0616]: field `balance_cents` of struct `Account` is private --> src/main.rs:24:34 | 24 | println!("balance: {}", acct.balance_cents); | ^^^^^^^^^^^^^ private field
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());}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:
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());}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:
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);}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.
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));}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:
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);}RED: 1 GREEN: 99 colors::GREEN: 2
If two globs bring the same name, using that name is an error until you choose one:
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());}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:
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(" "));}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:
fn main() { let mut stock = HashMap::new(); stock.insert("apples", 3); println!("stock: {stock:?}");}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:
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 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.rliband the programsappandtool. main.rsandlib.rsare different crates.main.rsnames the library by the package name (app::greet()),crate::inmain.rsmeansmain.rsitself, andpub(crate)stops at the end oflib.rs.my-utilis a dependency listed in[dependencies]; code names itmy_util, hyphen turned into underscore, and needs nomod. 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:
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 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.
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));}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.
#![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");}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:
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));}is_even(10): true is_odd(7): true
Packages may not, and so neither may their crates. Cargo refuses a dependency cycle between packages:
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()), "") ); }}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.
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
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");}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.
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
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");}ok
Read more
- The Rust Book, chapter 7: Packages, Crates and Modules, especially paths,
useand separating modules into files - The Reference: Visibility and privacy, Modules and Crates and source files
- Rust by Example: Modules, with visibility and file hierarchy
- Cargo: targets and workspaces
- The Book: test organization, for
#[cfg(test)]and thetests/folder - The standard library prelude
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.