Lesson 46 of 94
Modules & Visibility
So far every example has lived in one flat file. Real programs need structure: a way to group related code, hide implementation details, and expose a small public API. Rust does it with modules, use, and pub.
Three things to know up front:
- A Rust module is not tied to a directory. You can declare a module inline, in one file, or in a directory, and you always declare it explicitly with
mod. - Everything is private by default, and "private" means private to the module, not to the file or the class.
- Privacy is per module, not per type. A struct's private fields are visible to all code in the same module, including free functions. There is no "private to this class".
| Concept | Rust |
|---|---|
| module path | crate::billing |
| import | use crate::billing::Invoice; |
| public | pub |
| crate-wide visibility | pub(crate) |
| private (no modifier) | scoped to the module |
| group of related free functions | a module with free functions |
The examples fit in one file by declaring modules inline with mod name { ... }. Near the end we'll see how the same code is split across files, which changes nothing about how paths and visibility work.
1. Your first module
mod declares a module. Items inside it are reached with a path, where :: separates the parts.
mod greetings {
pub fn hello(name: &str) -> String {
format!("Hello, {name}!")
}
}
fn main() {
println!("{}", greetings::hello("Ana"));
}Hello, Ana!Without pub, hello would be private to greetings and main couldn't call it.
2. Private by default
Every item (function, struct, enum, constant, module) is private unless marked pub. Calling a private function from outside its module is a compile error.
mod billing {
pub fn total(prices: &[u32]) -> u32 {
prices.iter().sum::<u32>() + tax(prices)
}
fn tax(prices: &[u32]) -> u32 {
prices.iter().sum::<u32>() / 10
}
}
fn main() {
println!("{}", billing::total(&[100, 200]));
println!("{}", billing::tax(&[100, 200]));
}error[E0603]: function `tax` is private
--> src/main.rs:13:29
|
13 | println!("{}", billing::tax(&[100, 200]));
| ^^^ private function
|
note: the function `tax` is defined here
--> src/main.rs:6:5
|
6 | fn tax(prices: &[u32]) -> u32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^Rust's default is private: only the module itself (and its child modules, as we'll see) can use tax.
3. Nested modules: every step of the path must be visible
Modules nest. To use outer::inner::f from outside, inner must be pub as well as f. A pub fn inside a private module is still unreachable from outside.
mod shop {
mod pricing {
pub fn discount(price: u32) -> u32 {
price * 90 / 100
}
}
pub fn checkout(price: u32) -> u32 {
pricing::discount(price) // fine: `shop` can see its own child
}
}
fn main() {
println!("{}", shop::checkout(200));
println!("{}", shop::pricing::discount(200));
}error[E0603]: module `pricing` is private
--> src/main.rs:15:26
|
15 | println!("{}", shop::pricing::discount(200));
| ^^^^^^^ -------- function `discount` is not publicly re-exported
| |
| private module
|
note: the module `pricing` is defined here
--> src/main.rs:2:5
|
2 | mod pricing {
| ^^^^^^^^^^^Make the module public too, and the path works:
mod shop {
pub mod pricing {
pub fn discount(price: u32) -> u32 {
price * 90 / 100
}
}
}
fn main() {
println!("{}", shop::pricing::discount(200));
}1804. Children see their parents' private items
Privacy flows downward: a module can use every item of its ancestors, private or not. It can't see into a sibling's or a child's private items.
mod app {
const VERSION: &str = "2.4.1"; // private to `app`
pub mod banner {
pub fn show() {
// A child module can reach the parent's private items via `super`.
println!("MyApp v{}", super::VERSION);
}
}
}
fn main() {
app::banner::show();
}MyApp v2.4.1This is why unit tests live in a child module (mod tests) of the code they test: they can call private functions. The Testing lesson uses this a lot.
5. Siblings don't see each other's private items
orders and inventory are siblings under the crate root. orders can call inventory's public functions, but not its private ones.
mod inventory {
pub fn in_stock(sku: &str) -> bool {
count(sku) > 0
}
fn count(sku: &str) -> u32 {
if sku == "BOOK-1" { 3 } else { 0 }
}
}
mod orders {
pub fn place(sku: &str) {
if crate::inventory::in_stock(sku) {
println!("ordered {sku}, {} left", crate::inventory::count(sku) - 1);
}
}
}
fn main() {
orders::place("BOOK-1");
}error[E0603]: function `count` is private
--> src/main.rs:14:66
|
14 | println!("ordered {sku}, {} left", crate::inventory::count(sku) - 1);
| ^^^^^ private function
|
note: the function `count` is defined here
--> src/main.rs:6:5
|
6 | fn count(sku: &str) -> u32 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^6. Paths: crate::, self::, and super::
Paths can be absolute (start at the crate root with crate::) or relative (start at the current module, self::, or its parent, super::). The crate root is the file with main (or lib.rs for a library).
mod network {
pub fn connect() -> &'static str {
"connected"
}
pub mod client {
pub fn run() {
println!("absolute: {}", crate::network::connect());
println!("parent: {}", super::connect());
println!("self: {}", self::helper());
println!("plain: {}", helper()); // same as self::helper()
}
fn helper() -> &'static str {
"helper"
}
}
}
fn main() {
network::client::run();
}absolute: connected
parent: connected
self: helper
plain: helperPrefer crate:: for items far away and super:: for items in the parent. Absolute paths keep working if you move the calling code; super:: keeps working if you move a parent and child together.
7. use brings a path into scope
Typing full paths gets tedious. use creates a shortcut. It only affects the module where it's written.
mod geometry {
pub mod shapes {
#[derive(Debug)]
pub struct Rect {
pub w: f64,
pub h: f64,
}
pub fn area(r: &Rect) -> f64 {
r.w * r.h
}
}
}
use geometry::shapes;
use geometry::shapes::Rect;
fn main() {
let r = Rect { w: 3.0, h: 4.0 };
println!("{r:?} has area {}", shapes::area(&r));
}Rect { w: 3.0, h: 4.0 } has area 12The Rust convention:
- For functions, import the parent module and call
shapes::area(...). The prefix shows the function isn't local. - For types (structs, enums, traits), import the type itself:
Rect,HashMap,Duration.
That matches how you already use the standard library: use std::collections::HashMap; and then HashMap::new(), but use std::mem; and then mem::swap(...).
8. Grouped imports, self, and as
Several imports from one parent can share a line with braces. self inside the braces imports the parent module itself. as renames an import, which solves name clashes.
use std::collections::{self, BTreeMap, HashMap};
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;
fn describe(f: &mut std::fmt::Formatter) -> FmtResult {
write!(f, "custom")
}
fn read_config() -> IoResult<String> {
Ok(String::from("port=8080"))
}
fn main() {
let mut a: HashMap<&str, i32> = HashMap::new();
let mut b: BTreeMap<&str, i32> = BTreeMap::new();
let c: collections::HashSet<i32> = collections::HashSet::from([1]);
a.insert("x", 1);
b.insert("y", 2);
let _ = describe;
println!("{a:?} {b:?} {c:?} {:?}", read_config());
}{"x": 1} {"y": 2} {1} Ok("port=8080")9. use inside a function
use can appear in any scope, including a single function. This keeps a rarely used import next to the code that needs it.
fn top_scores(scores: &mut Vec<u32>, n: usize) -> &[u32] {
use std::cmp::Reverse;
scores.sort_by_key(|&s| Reverse(s));
&scores[..n.min(scores.len())]
}
fn main() {
let mut scores = vec![72, 95, 88, 61, 99];
println!("{:?}", top_scores(&mut scores, 3));
}[99, 95, 88]10. Glob imports and use super::*
use path::* imports every public item. Avoid it in normal code because it hides where names come from. Two places where it's idiomatic: use super::*; in a test module, and a crate's prelude (next example).
mod temperature {
pub fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
pub fn f_to_c(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
pub mod checks {
use super::*; // everything from `temperature`, private items included
pub fn round_trip(c: f64) -> bool {
(f_to_c(c_to_f(c)) - c).abs() < 1e-9
}
}
}
use temperature::*;
fn main() {
println!("{:.1} F", c_to_f(21.5));
println!("round trip ok: {}", checks::round_trip(-40.0));
}70.7 F
round trip ok: true11. A prelude module
Libraries often offer a prelude module that re-exports the items almost every user needs, so users write one glob import. The standard library does this for you automatically: Vec, String, Option, Some, Ok, Clone, and friends come from std::prelude.
mod shop {
pub mod model {
#[derive(Debug)]
pub struct Product {
pub name: String,
pub cents: u64,
}
}
pub mod money {
pub fn format_cents(cents: u64) -> String {
format!("${}.{:02}", cents / 100, cents % 100)
}
}
pub mod prelude {
pub use super::model::Product;
pub use super::money::format_cents;
}
}
use shop::prelude::*;
fn main() {
let p = Product { name: String::from("Keyboard"), cents: 4999 };
println!("{} costs {}", p.name, format_cents(p.cents));
}Keyboard costs $49.9912. Struct fields have their own visibility
Making a struct pub does not make its fields public. Each field needs its own pub. Code outside the module can't read, write, or even name a private field, and so it can't construct the struct with a literal.
mod users {
pub struct User {
pub name: String,
password_hash: String,
}
}
fn main() {
let u = users::User { name: String::from("ana"), password_hash: String::from("x") };
println!("{}", u.name);
}error[E0451]: field `password_hash` of struct `User` is private
--> src/main.rs:9:54
|
9 | let u = users::User { name: String::from("ana"), password_hash: String::from("x") };
| ^^^^^^^^^^^^^ private fieldThis is encapsulation (private fields plus public methods) applied at the module level.
13. Constructors enforce invariants
Private fields plus a public constructor let a module guarantee something about every value of a type. Outside code can only get an Email through Email::parse, so every Email in the program is valid.
mod contact {
#[derive(Debug)]
pub struct Email {
address: String,
}
impl Email {
pub fn parse(raw: &str) -> Result<Email, String> {
let raw = raw.trim();
match raw.split_once('@') {
Some((user, domain)) if !user.is_empty() && domain.contains('.') => {
Ok(Email { address: raw.to_lowercase() })
}
_ => Err(format!("invalid email: {raw:?}")),
}
}
pub fn domain(&self) -> &str {
self.address.split_once('@').map(|(_, d)| d).unwrap_or("")
}
pub fn as_str(&self) -> &str {
&self.address
}
}
}
use contact::Email;
fn main() {
for raw in [" Ana@Example.COM ", "bob@", "@x.io", "carol@mail.org"] {
match Email::parse(raw) {
Ok(e) => println!("ok: {} (domain {})", e.as_str(), e.domain()),
Err(msg) => println!("{msg}"),
}
}
}ok: ana@example.com (domain example.com)
invalid email: "bob@"
invalid email: "@x.io"
ok: carol@mail.org (domain mail.org)14. Tuple structs with private fields
A tuple struct's fields are private too unless marked pub. With a private field, even the constructor UserId(5) is unavailable outside the module.
mod ids {
#[derive(Debug, Clone, Copy)]
pub struct UserId(u64);
pub fn next() -> UserId {
UserId(42)
}
}
fn main() {
let a = ids::next();
let b = ids::UserId(7);
println!("{a:?} {b:?}");
}error[E0603]: tuple struct constructor `UserId` is private
--> src/main.rs:12:18
|
3 | pub struct UserId(u64);
| --- a constructor is private if any of the fields is private
...
12 | let b = ids::UserId(7);
| ^^^^^^ private tuple struct constructor
|
note: the tuple struct constructor `UserId` is defined here
--> src/main.rs:3:5
|
3 | pub struct UserId(u64);
| ^^^^^^^^^^^^^^^^^^^^^^^
help: consider making the field publicly accessible
|
3 | pub struct UserId(pub u64);
| +++A common pattern is pub struct Meters(pub f64); for a transparent wrapper, and a private field when the wrapper must stay valid.
15. Enum variants are always public
Unlike struct fields, an enum's variants and their fields are all public when the enum is. There's no way to hide one variant: a match outside the module must be able to see all of them.
mod http {
#[derive(Debug)]
pub enum Status {
Ok,
NotFound { path: String },
Error(u16),
}
pub fn fetch(path: &str) -> Status {
match path {
"/" => Status::Ok,
"/boom" => Status::Error(500),
other => Status::NotFound { path: other.to_string() },
}
}
}
use http::Status;
fn main() {
for path in ["/", "/missing", "/boom"] {
let text = match http::fetch(path) {
Status::Ok => String::from("200 OK"),
Status::NotFound { path } => format!("404 {path}"),
Status::Error(code) => format!("{code} error"),
};
println!("{text}");
}
}200 OK
404 /missing
500 error16. Methods have their own visibility
A pub type can have private methods. Private methods are helpers for the type's own module, and every function in the module can call them.
mod cache {
pub struct Cache {
hits: u32,
}
impl Cache {
pub fn new() -> Self {
Cache { hits: 0 }
}
pub fn get(&mut self, key: &str) -> Option<&'static str> {
self.record_hit();
if key == "home" { Some("<html>home</html>") } else { None }
}
fn record_hit(&mut self) {
self.hits += 1;
}
}
}
fn main() {
let mut c = cache::Cache::new();
c.get("home");
c.record_hit();
}error[E0624]: method `record_hit` is private
--> src/main.rs:25:7
|
16 | fn record_hit(&mut self) {
| ------------------------ private method defined here
...
25 | c.record_hit();
| ^^^^^^^^^^ private method17. Privacy is per module, not per type
In Rust, private means "only this module", so a free function next to a struct can touch its private fields. This is often handy: a helper doesn't have to be a method.
mod account {
pub struct Account {
owner: String,
balance: i64,
}
impl Account {
pub fn open(owner: &str) -> Self {
Account { owner: owner.to_string(), balance: 0 }
}
}
// Not a method, but in the same module, so it can use the private fields.
pub fn transfer(from: &mut Account, to: &mut Account, amount: i64) -> Result<(), String> {
if from.balance < amount {
return Err(format!("{} has only {}", from.owner, from.balance));
}
from.balance -= amount;
to.balance += amount;
Ok(())
}
pub fn deposit(acc: &mut Account, amount: i64) {
acc.balance += amount;
}
pub fn report(acc: &Account) -> String {
format!("{}: {}", acc.owner, acc.balance)
}
}
fn main() {
let mut a = account::Account::open("ana");
let mut b = account::Account::open("ben");
account::deposit(&mut a, 100);
println!("{:?}", account::transfer(&mut a, &mut b, 30));
println!("{:?}", account::transfer(&mut b, &mut a, 50));
println!("{} | {}", account::report(&a), account::report(&b));
}Ok(())
Err("ben has only 30")
ana: 70 | ben: 30If you want "private to this type", put the type in its own small module.
18. pub(crate), pub(super), and pub(in path)
Between private and fully public there are scoped levels:
pub(crate): visible anywhere in this crate, but not to other crates that depend on it.pub(super): visible to the parent module.pub(in crate::some::module): visible within that module (which must be an ancestor).
mod engine {
pub(crate) fn version() -> &'static str {
"1.0"
}
pub mod parser {
pub(super) fn tokenize(src: &str) -> Vec<&str> {
src.split_whitespace().collect()
}
pub(in crate::engine) fn token_count(src: &str) -> usize {
tokenize(src).len()
}
}
pub fn run(src: &str) {
// Both are visible here: `engine` is the parent of `parser`.
println!("tokens: {:?}", parser::tokenize(src));
println!("count: {}", parser::token_count(src));
}
}
fn main() {
println!("engine {}", engine::version()); // pub(crate): fine anywhere in the crate
engine::run("let x = 5");
// engine::parser::tokenize("x"); // error: only visible to `engine`
}engine 1.0
tokens: ["let", "x", "=", "5"]
count: 419. pub(super) outside its scope is an error
The restricted levels are enforced like plain privacy. Here main lives at the crate root, which is not the parent of parser, so tokenize is out of reach.
mod engine {
pub mod parser {
pub(super) fn tokenize(src: &str) -> Vec<&str> {
src.split_whitespace().collect()
}
}
}
fn main() {
println!("{:?}", engine::parser::tokenize("a b"));
}error[E0603]: function `tokenize` is private
--> src/main.rs:10:38
|
10 | println!("{:?}", engine::parser::tokenize("a b"));
| ^^^^^^^^ private function
|
note: the function `tokenize` is defined here
--> src/main.rs:3:9
|
3 | pub(super) fn tokenize(src: &str) -> Vec<&str> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^20. pub use: re-exporting a flat public API
pub use imports an item and re-exports it, so users can reach it through the new path. Libraries use it to keep a deep internal structure while offering a short public path.
mod payments {
mod gateway {
pub mod stripe {
pub fn charge(cents: u64) -> String {
format!("stripe charged {cents}")
}
}
pub mod paypal {
pub fn charge(cents: u64) -> String {
format!("paypal charged {cents}")
}
}
}
mod model {
#[derive(Debug)]
pub struct Receipt {
pub id: u32,
pub text: String,
}
}
// The public face of the module: users never see `gateway` or `model`.
pub use gateway::paypal::charge as paypal_charge;
pub use gateway::stripe::charge;
pub use model::Receipt;
}
fn main() {
let r = payments::Receipt { id: 1, text: payments::charge(1999) };
println!("{r:?}");
println!("{}", payments::paypal_charge(500));
}Receipt { id: 1, text: "stripe charged 1999" }
paypal charged 500The standard library does this constantly: std::collections::HashMap is really defined in std::collections::hash::map, and re-exported.
21. Traits must be in scope to call their methods
A trait's methods are only callable where the trait is in scope. The most common surprise: write! into a String needs std::fmt::Write.
fn main() {
let mut report = String::new();
write!(report, "{} items", 3).unwrap();
println!("{report}");
}error[E0599]: cannot write into `String`
--> src/main.rs:3:12
|
3 | write!(report, "{} items", 3).unwrap();
| ^^^^^^
|
--> /rustc/<version>/library/core/src/fmt/mod.rs:212:7
|
= note: the method is available for `String` here
note: must implement `io::Write`, `fmt::Write`, or have a `write_fmt` method
--> src/main.rs:3:12
|
3 | write!(report, "{} items", 3).unwrap();
| ^^^^^^
= help: items from traits can only be used if the trait is in scope
help: trait `Write` which provides `write_fmt` is implemented but not in scope; perhaps you want to import it
|
1 + use std::fmt::Write;
|Import the trait (not the type) and it works:
use std::fmt::Write;
fn main() {
let mut report = String::new();
write!(report, "{} items", 3).unwrap();
writeln!(report, ", total ${:.2}", 59.97).unwrap();
print!("{report}");
}3 items, total $59.97Methods from traits are only visible where the trait is imported. That's also why a crate prelude usually re-exports its traits.
22. The same trait method from two traits
Two traits in scope can both define a method with the same name. Import each one where you need it, or call with a path to pick one explicitly.
mod export {
pub trait Csv {
fn render(&self) -> String;
}
pub trait Json {
fn render(&self) -> String;
}
pub struct Point {
pub x: i32,
pub y: i32,
}
impl Csv for Point {
fn render(&self) -> String {
format!("{},{}", self.x, self.y)
}
}
impl Json for Point {
fn render(&self) -> String {
format!("{{\"x\":{},\"y\":{}}}", self.x, self.y)
}
}
}
use export::{Csv, Json, Point};
fn main() {
let p = Point { x: 3, y: -1 };
// p.render() would be ambiguous here: both traits are in scope.
println!("{}", Csv::render(&p));
println!("{}", Json::render(&p));
}3,-1
{"x":3,"y":-1}23. Constants, statics, and types in modules
Modules hold more than functions: constants, statics, type aliases, and traits all live in modules and follow the same privacy rules.
mod limits {
pub const MAX_UPLOAD_MB: u32 = 25;
pub const ALLOWED_TYPES: [&str; 3] = ["png", "jpg", "pdf"];
pub type Megabytes = u32;
pub fn check(name: &str, size: Megabytes) -> Result<(), String> {
let ext = name.rsplit('.').next().unwrap_or("");
if !ALLOWED_TYPES.contains(&ext) {
return Err(format!("{name}: type .{ext} not allowed"));
}
if size > MAX_UPLOAD_MB {
return Err(format!("{name}: {size} MB exceeds {MAX_UPLOAD_MB} MB"));
}
Ok(())
}
}
fn main() {
for (name, size) in [("cat.png", 2), ("movie.mp4", 700), ("scan.pdf", 40)] {
match limits::check(name, size) {
Ok(()) => println!("{name}: accepted"),
Err(e) => println!("{e}"),
}
}
}cat.png: accepted
movie.mp4: type .mp4 not allowed
scan.pdf: 40 MB exceeds 25 MB24. Splitting modules into files
Everything so far used inline mod name { ... }. In a real project, mod name; (with a semicolon) tells the compiler to load the module body from a file. This layout:
src/
├── main.rs crate root
├── config.rs mod config
└── store/
├── mod.rs mod store (or store.rs next to the store/ directory)
├── memory.rs mod store::memory
└── disk.rs mod store::diskis declared like this:
// src/main.rs
mod config; // loads src/config.rs
mod store; // loads src/store.rs or src/store/mod.rs
fn main() {
let cfg = config::load();
let s = store::open(&cfg);
s.put("k", "v");
}// src/store/mod.rs (or src/store.rs)
mod disk; // loads src/store/disk.rs
mod memory; // loads src/store/memory.rs
pub use memory::MemoryStore;
pub fn open(cfg: &crate::config::Config) -> MemoryStore {
MemoryStore::with_capacity(cfg.capacity)
}Things to know:
- A file is not a module until some parent declares it with
mod. A.rsfile nobody declares is simply not compiled. - The module path comes from the
moddeclarations, not from the directory alone. The file is only where the body is stored. store.rs+store/andstore/mod.rsare two spellings of the same thing. Newer code tends to usestore.rsso that you don't end up with many tabs namedmod.rs.- One file can hold many types. There is no "one public type per file" rule.
25. Realistic: a layered application
A typical service split into domain, repository, and service layers. Only the service's API is public; the repository is hidden behind it, and Order can only be created through Order::new.
mod domain {
#[derive(Debug, Clone)]
pub struct Order {
id: u32,
item: String,
qty: u32,
}
impl Order {
pub fn new(id: u32, item: &str, qty: u32) -> Result<Order, String> {
if qty == 0 {
return Err(String::from("quantity must be positive"));
}
Ok(Order { id, item: item.to_string(), qty })
}
pub fn id(&self) -> u32 {
self.id
}
pub fn summary(&self) -> String {
format!("#{} {} x{}", self.id, self.item, self.qty)
}
}
}
mod repository {
use crate::domain::Order;
use std::collections::HashMap;
#[derive(Default)]
pub(crate) struct OrderRepo {
rows: HashMap<u32, Order>,
}
impl OrderRepo {
pub(crate) fn save(&mut self, order: Order) {
self.rows.insert(order.id(), order);
}
pub(crate) fn find(&self, id: u32) -> Option<&Order> {
self.rows.get(&id)
}
}
}
pub mod service {
use crate::domain::Order;
use crate::repository::OrderRepo;
#[derive(Default)]
pub struct OrderService {
repo: OrderRepo,
next_id: u32,
}
impl OrderService {
pub fn place(&mut self, item: &str, qty: u32) -> Result<u32, String> {
let id = self.next_id + 1;
let order = Order::new(id, item, qty)?; // `?` returns early: the id is not used up
self.repo.save(order);
self.next_id = id;
Ok(id)
}
pub fn describe(&self, id: u32) -> String {
match self.repo.find(id) {
Some(o) => o.summary(),
None => format!("#{id} not found"),
}
}
}
}
use service::OrderService;
fn main() {
let mut svc = OrderService::default();
let a = svc.place("book", 2).unwrap();
println!("{:?}", svc.place("pen", 0));
println!("{}", svc.describe(a));
println!("{}", svc.describe(99));
}Err("quantity must be positive")
#1 book x2
#99 not found26. Realistic: a unit-safe money module
The money module guarantees that amounts are never negative and never mix currencies. The fields are private, so outside code can't build a bad Money, and add is the only way to combine them.
mod money {
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Currency {
Eur,
Usd,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Money {
cents: u64,
currency: Currency,
}
impl Money {
pub fn new(amount: f64, currency: Currency) -> Option<Money> {
if amount.is_finite() && amount >= 0.0 {
Some(Money { cents: (amount * 100.0).round() as u64, currency })
} else {
None
}
}
pub fn add(self, other: Money) -> Result<Money, String> {
if self.currency != other.currency {
return Err(format!("cannot add {:?} to {:?}", other.currency, self.currency));
}
Ok(Money { cents: self.cents + other.cents, currency: self.currency })
}
}
impl fmt::Display for Money {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let symbol = match self.currency {
Currency::Eur => "€",
Currency::Usd => "$",
};
write!(f, "{symbol}{}.{:02}", self.cents / 100, self.cents % 100)
}
}
}
use money::{Currency, Money};
fn main() {
let a = Money::new(19.99, Currency::Usd).unwrap();
let b = Money::new(5.01, Currency::Usd).unwrap();
let c = Money::new(3.0, Currency::Eur).unwrap();
println!("{}", a.add(b).unwrap());
println!("{:?}", a.add(c));
println!("{:?}", Money::new(-1.0, Currency::Eur));
}$25.00
Err("cannot add Eur to Usd")
None27. Realistic: a plugin registry with a sealed trait
Sometimes you want a public trait that outsiders can use but not implement, so you can add methods later without breaking anyone. The sealed trait pattern does this with a private module: the trait requires a supertrait that nobody outside the crate can name.
mod formats {
mod sealed {
pub trait Sealed {}
}
// Public trait, but implementing it needs `sealed::Sealed`, which is unreachable outside `formats`.
pub trait Format: sealed::Sealed {
fn name(&self) -> &'static str;
fn encode(&self, fields: &[(&str, &str)]) -> String;
}
pub struct Csv;
pub struct Query;
impl sealed::Sealed for Csv {}
impl sealed::Sealed for Query {}
impl Format for Csv {
fn name(&self) -> &'static str {
"csv"
}
fn encode(&self, fields: &[(&str, &str)]) -> String {
fields.iter().map(|(_, v)| *v).collect::<Vec<_>>().join(",")
}
}
impl Format for Query {
fn name(&self) -> &'static str {
"query"
}
fn encode(&self, fields: &[(&str, &str)]) -> String {
fields.iter().map(|(k, v)| format!("{k}={v}")).collect::<Vec<_>>().join("&")
}
}
pub fn all() -> Vec<Box<dyn Format>> {
vec![Box::new(Csv), Box::new(Query)]
}
}
fn main() {
let fields = [("name", "ana"), ("lang", "rust")];
for f in formats::all() {
println!("{:>5}: {}", f.name(), f.encode(&fields));
}
} csv: ana,rust
query: name=ana&lang=rustOutside formats, writing impl formats::Format for MyType fails because MyType can't implement the private Sealed trait.
Summary
mod name { ... }declares a module inline;mod name;loads it fromname.rsorname/mod.rs. Files are only compiled when amoddeclares them.- Everything is private to its module by default. Child modules see their ancestors' private items; siblings and parents don't see a child's private items.
pubopens an item up.pub(crate),pub(super), andpub(in path)open it to a limited scope.- Struct fields and methods have their own visibility; enum variants are public with the enum. Private fields plus a constructor let a module enforce invariants.
- Paths start with
crate::(absolute),self::, orsuper::(relative).usecreates shortcuts;asrenames;pub usere-exports. - Traits must be in scope to call their methods.
Next: Crates, Packages & Workspaces shows how modules fit into crates, how Cargo manages dependencies and features, and how to structure multi-crate projects.
