Lesson 12 / 12
Modules and Crates
mod and use are both items: neither produces a value on its own, one draws a name boundary, the other shortens an existing path. The same name is called both from inside and outside the module, and pub versus non-pub cases are compared; two separate source files are compiled with a single rustc call and produce a single crate. The limiting measurement shows that visibility must be granted separately at every link of the path: even if an item is pub, it is unreachable from outside if the module wrapping it is not pub.
Published Updated
In this lesson
- Shows that mod and use declarations are items and produce no value on their own
- Compares calling the same name from inside and outside a module, with and without pub
- Measures the difference between a module and a crate by how two source files merge into a single compilation unit
Contents
Those five data-modeling lessons closed having asked one question throughout: does
something written in the source produce a value? This question never once touched on
where in the source a type or a function appears — a struct declaration, a map
variable, a function, which boundary is it defined within and how is it seen from outside
that boundary? The Setup and the Package Manager lesson deferred this question once
already: it said that the crate root is not a single file in a real project, that the
compiler starts there and reaches other files through mod declarations, but it did not
show this. This lesson — the course’s wrap-up — opens that deferred question: do mod and
use produce a value, or do they only draw a boundary; and is a module’s boundary the same
thing as a crate’s boundary?
mod Draws a Boundary, It Produces No Value Itself
Rust’s Design Goals lesson already showed a module in the form mod measure { pub fn unit() -> i32 { 3 } }, and that lesson’s measurement core counted the mod declaration as
an item — but did not dwell on it. This lesson stops exactly there: the line mod name { ... } produces nothing when it runs. Just like the struct declaration in the Structs
lesson, it introduces the compiler to a name and a boundary, it is not itself evaluated.
Every item inside it — a function, a constant, another module — only bears a value once it
is used.
// mod_inside_outside.rs — mod is an item, the same name is called from inside and outside the module
mod measurement {
pub fn unit() -> i32 {
3
}
fn private_factor() -> i32 {
10
}
pub fn scaled() -> i32 {
unit() * private_factor()
}
}
fn main() {
let from_outside = measurement::unit();
println!("{:<36}: {}", "measurement::unit() (pub, outside)", from_outside);
let from_inside = measurement::scaled();
println!("{:<36}: {}", "measurement::scaled() (pub, outside)", from_inside);
}
measurement::unit() (pub, outside) : 3 measurement::scaled() (pub, outside): 30
DM20. measurement’s declaration never ran on any line; both outputs seen come from
the lines where unit and scaled are called. The name unit is called here from two
separate places: from inside main as measurement::unit() — from outside the module, by
the full path — and from scaled’s body as unit() — from inside the module, with no
prefix needed. The same name, the same item, produces the same value (3) from two
different places; what changes is only which side of the module boundary the caller stands
on.
The private_factor function was declared without pub, and it could only be called from
scaled’s body — that is, from inside the measurement module itself. This shows that
visibility in Rust is closed by default: an item is invisible from outside the module it
is defined in unless a single keyword — pub — is added. unit and scaled were
reachable from main because they carry this keyword; private_factor was not reachable
because it does not. If measurement::private_factor() were written inside main, this
line would not compile. This rejection is not placed in a code block here — for the same
reason established in the Pattern Matching lesson, source that fails to compile does not
enter a block — but the reason can be described: the compiler sees that the call is trying
to cross a module boundary through an item that does not carry pub, and rejects the
source.
A File Boundary Becomes a Module Boundary — a Crate Is a Single Compilation Unit
The Setup and the Package Manager lesson said that the crate root is always a single file
in this course, but that in a real project the compiler starts there and reaches other
files through mod declarations. It is shown here: the syntax mod name; — bodyless,
ending only in a semicolon — tells the compiler to read the file name.rs in the same
directory and treat that file’s entirety as the body of the module named name.
// helper.rs — the entire file pulled in by a mod name is the module's body (SOURCE)
pub fn visible_outside() -> i32 {
private_constant() + 5
}
fn private_constant() -> i32 {
100
}
// mod_file_boundary.rs — mod helper; turns a file boundary into a module boundary
mod helper;
fn main() {
let value = helper::visible_outside();
println!("{:<34}: {}", "helper::visible_outside()", value);
}
helper::visible_outside() : 105
DM21. There are two separate source files in this measurement — mod_file_boundary.rs
and helper.rs — but the compile command ran once and produced a single binary: two files,
one crate. The name of this single compilation unit is the crate: the whole formed by
starting from a single source root given to rustc and gathering every file reached
through mod, passed through a single compilation step. The private_constant function
inside helper.rs does not carry pub; the same rule from the previous section applies
here too — private_constant can only be called from inside the helper module itself,
that is, from visible_outside’s body.
A module and a crate can be confused here, because both draw a boundary, but the boundaries
they draw are on different planes. A module is a name and visibility boundary; it does not
care whether the source sits in a single file or in ten — the measurement module in the
previous section showed this inside a single file. A crate, on the other hand, is a
compilation and linking boundary: however many files, however many modules it is made of,
everything that passes through a single rustc call belongs to the same crate. A crate can
carry more than one module — mod_file_boundary.rs’s crate root and the helper module
were two parts of the same crate here — but a module is never split across more than one
crate.
use Is an Item: the Shortcut Itself Produces No Value
Once the boundary mod draws is established, writing a name inside that boundary with its
full path (measurement::unit()) every single time is not mandatory. A use declaration is
a name that shortens a path, and just like mod, it is an item: the line itself produces no
value, it only introduces a short name the following lines can use.
// use_item.rs — use is an item, a value is born only once the shortcut name is used
mod measurement {
pub fn unit() -> i32 {
3
}
}
use measurement::unit;
fn main() {
let via_shortcut = unit();
let via_full_path = measurement::unit();
println!("{:<31}: {}", "unit() (via use shortcut)", via_shortcut);
println!("{:<31}: {}", "measurement::unit() (full path)", via_full_path);
println!("{:<31}: {}", "equal", via_shortcut == via_full_path);
}
unit() (via use shortcut) : 3 measurement::unit() (full path): 3 equal : true
DM22. The line use measurement::unit; sits before main, and no output is directly
tied to it — just like mod helper; in the previous section, it is not itself run. Both
calls — unit() via the shortcut, and measurement::unit() via the full path — produce the
same value (3); the equal line confirms this directly. use introduces a shortcut, not
a new computation or a new behavior: there is no difference between the value coming through
the shortcut and the value coming through the full path, because both point to the same
item, measurement::unit. This belongs to the same family as the previous section’s
finding — a declaration adds no new computation to the source, it only adds a name the
following lines can read; mod does this for a boundary, use does the same for a path
inside an existing boundary.
Limiting Measurement: Visibility Must Be Granted Separately at Every Link of the Path
The two measurements so far showed visibility at a single level: an item either carried
pub out to the module or it did not. The real boundary is a little more fine-grained than
that: even if an item is pub, if the module wrapping it is not pub, that item cannot be
reached from outside.
// visibility_chain.rs — visibility must be granted separately at every link of the path
mod outer {
pub mod inner {
pub fn value() -> i32 {
hidden() + 1
}
fn hidden() -> i32 {
6
}
}
}
fn main() {
let result = outer::inner::value();
println!("{:<37}: {}", "outer::inner::value() (two pub links)", result);
}
outer::inner::value() (two pub links): 7
DM23. The call outer::inner::value() compiled and gave 7, because both links in the
path — the inner module and the value function — carry pub; outer itself does not
carry pub, but it is already visible from outside (from this same file) because it is
defined at the crate root’s own level. If inner had been written without pub, only as
mod inner { ... }, the call outer::inner::value() would not compile even though the
value function itself still carried pub — because the path would have to pass through a
module boundary that does not carry pub. This source, too, is not placed in a code block,
by the same not-compiling-source rule; but its result can be described: for the compiler, a
single item’s visibility is not enough — the visibility of every step along the path must be
granted separately.
This is also the boundary of the course’s own wrap-up. The three measurements here — the
single-level pub/private contrast, a file boundary turning into a module boundary, a
multi-link visibility chain — treated visibility as a binary switch (open or closed).
Rust’s intermediate grades — visible only within the crate, visible only to the parent
module, and so on — were never measured in this lesson, because the measurement axis was
still the same question: does what is written in the source produce a value. The difference
between pub and non-pub written in the source produced a single binary answer; the finer
grades within that binary answer are a separate subject, outside this axis.
Summary
- A
moddeclaration is an item: it produces no value itself, it only introduces a name and a boundary; the same name can be called without a prefix from inside the module and by full path from outside. - Visibility is closed by default; a single
pubkeyword opens an item to the outside of its module. - The syntax
mod name;turns a file boundary into a module boundary; several source files can be compiled in a singlerustccall and form a single crate. useis an item: it introduces a name that shortens a path, it produces no value itself; the same item called through the shortcut and through the full path gives the same value.- Visibility must be granted separately at every link of the path: even if an item is
pub, it is unreachable from outside if a module wrapping it is notpub.
Course Wrap-Up
This course carried a single question through twelve lessons: does something written in the source produce a value, or does it only declare a name? The first six lessons put this question to the language’s smallest units — expressions, bindings, primitive types, control flow; the next five lessons carried the same question to the data-modeling tools — structs, enums, pattern matching, collections, ordered and hash structures. This last lesson added, without changing the question itself, the question of where in the source something appears: which boundary is an item defined within, and how is it seen from outside that boundary?
Each of the twelve lessons measured its own slice and wrote its own limiting measurement; the table below gathers all twelve measurements in one place.
| Lesson | Writing measured | Class (expression · statement · item) | Limiting measurement |
|---|---|---|---|
| Rust’s Design Goals | if/match picks a branch and gives a value |
expression | Class is read not from the nature of the work but from the method’s signature (insert expression, push statement) |
| Setup and the Package Manager | entry point, crate root, manifest | item | The manifest is not inside the source; only the output’s name changes when just that file changes, with the source unchanged |
| Variables and Immutability | a let binding |
statement | Immutability is the binding’s own boundary; sharing the data another way is a separate question, the subject of the next course |
| Primitive Types | constructing a value from one of five primitive types | expression | () is not valueless, it is a single-valued type |
| Functions and Expression Orientation | the body’s last line (tail position) | expression | A semicolon silently drops the value; the computation still happens |
| Control Flow | an if without else |
expression | Its value is always (); the source fails to compile if its body tries to produce another type |
| Structs | a struct declaration |
item | Position is the sole identity in a tuple struct; field order is free in a named struct |
| Enums | an enum declaration |
item | Variant count is not state count; a data-carrying variant carries an uncountable number of states |
| Pattern Matching | a match expression |
expression | A catch-all arm provides exhaustiveness but does not protect: it silently covers a variant added later without handling it with its own logic |
| Collections | push / insert |
statement · expression | If insert’s return value is not bound to a variable, the old value it overwrites is silently lost |
| Ordered and Set Structures | set insertion (insert → bool) |
expression | The ordering guarantee depends on a condition: the key type has to satisfy the ordering requirement |
| Modules and Crates | mod declaration / use |
item | Visibility must be granted separately at every link of the path; an item is unreachable if the module wrapping it is not, even if the item itself is pub |
The three classes — expression, statement, item — never changed from the start of the
course to its end; what changed was only the breadth of the writing these three classes
were applied to: from a single line to a function body, from a data type’s declaration to a
module’s boundary. Ownership and borrowing were deferred again and again throughout this
course — a binding’s immutability (the Variables and Immutability lesson), a receiver’s
&self sign (the Structs lesson), a reference’s double & sign (the Collections
lesson) — each time passed over by saying this was the subject of Ownership and the Memory
Model. That course
opens this deferred question: who owns a value, what does borrowing a reference mean, and
what happens when a value is separated from its owner?
To keep your progress and take notes, Log in
My notes
Log in to take notes.