One module per file¶
Level: 201 · working knowledge
One line: mod name; is a declaration that the module's contents are in the file next door — not an #include, not an import — and the module tree it builds is identical to the one you would get from mod blocks in a single file.
src/
main.rs mod election;
election.rs mod ballots; mod tally;
election/
ballots.rs mod spoiled;
ballots/
spoiled.rs
tally.rs
election.rs beside election/ is the 2018-edition layout and the one to use. The older form puts election/mod.rs in the directory instead; it still works, and a project with eight files called mod.rs open in your editor is why it was changed.
The paths do not change when you split¶
Those two lines are the same in both layouts, and the same again if the whole thing is mod blocks in one file. The module tree is a property of the mod declarations, not of the directories — which is what makes moving a module between a block and a file a pure refactor, with no call site touched.
| From | Write |
|---|---|
| the crate root, absolutely | crate::election::ballots::count() |
| a sibling module | super::ballots::count() |
| a child, reaching its parent | super::count() |
| the same module, explicitly | self::total() |
Reach for crate:: when the item is far away or the module might move — an absolute path survives a rename of the module you are in. Reach for super:: for a sibling, once; two super::super:: in a row is a sign the tree is wrong rather than that you need a third.
The crate root¶
A binary's root is src/main.rs; a library's is src/lib.rs. That file is the crate's top module, so an item declared there sits at crate::name and needs no module of its own. A package with both files is two crates — a library, and a binary that uses it by name rather than by path, which is why you see use my_crate::… in main.rs and use crate::… inside lib.rs.
The trap: a file nobody declared¶
Add src/election/recount.rs, forget mod recount; in election.rs, and the file is not part of the crate. It is not compiled, so its syntax errors never appear; its tests never run; nothing warns; cargo build succeeds. This is the most common "why is my new code not running" in a first Rust project, and the answer is always the missing declaration.
The mirror failure is E0428 — declaring the same module twice, from two places, which is a duplicate definition rather than the "included twice" that a C header would give you.
If you are coming from another language¶
- Python. The file layout looks identical — a package is a directory, a module is a file — and the mechanism underneath is completely different. Python discovers modules by searching
sys.pathat run time; Rust is told about them at compile time bymod, and a file nobody names is invisible. That is the trap above, and it is the exact inverse of Python's, where the file is found and the import is what fails. Two more:__init__.pyiselection.rs/mod.rs, but it holds real code and declarations rather than usually being empty; and there are no circular-import problems, because nothing executes at declaration time. - ABAP. There are no files to organise — code lives in the repository, addressed by object name, and the "tree" is the package hierarchy in SE80. The nearest counterpart to
modis a local class or include inside a program, and to the crate root, the program or class pool itself. What transfers is the package-check discipline: an ABAP package withPackage Check as Serveron ispub(crate)for its contents, and a package interface is the list of what it exports — which is exactlypubon amod. What does not transfer is the sequencing: anINCLUDEreally does splice text at that point, so an include used twice compiles twice;mod x;names a module once, and a second declaration isE0428. - C / C++.
modis emphatically not#include. There is no text splicing, no include guards, no order dependence, and no way for a module to be compiled twice. It is closer to a C++20 module declaration, or to the way a build system lists source files — except that the list lives in the source rather than in the build file, which is why Cargo needs no source list at all. - Java. One public class per file and a directory per package is the same convention enforced; Rust enforces neither and gets the tree from the declarations instead, so a module may hold ten types and a file may hold three modules.
The verified output¶
Verified output of one_module_per_file.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The tree above, as files
src/
main.rs <- the crate root: `mod election;`
election.rs <- `mod ballots;` and `mod tally;`
election/
ballots.rs
tally.rs
Or, in the older layout that still works:
election/mod.rs instead of election.rs
Both are supported. `election.rs` beside `election/` is the
2018-edition form and the one to use; `mod.rs` is what you will
meet in older code, and a directory full of files called mod.rs
is why it was changed.
2. `mod election;` is a declaration
It says "there is a module called election, its contents are in
the file next door". It is NOT #include, and NOT an import: the
file is compiled as part of this crate exactly once, and a second
`mod election;` elsewhere is E0428, a duplicate definition.
A .rs file nobody declares is not compiled at all — the most
common way a new file appears to do nothing.
3. The paths do not change when you split
election::summary() = 12 ballots, 60 points
election::tally::total() = 60
Those two lines are identical in both layouts. The module tree
is a property of the `mod` declarations, not of the directories,
which is why moving a module between file and block is a pure
refactor.
4. The crate root, and what `crate::` means
A binary's root is src/main.rs; a library's is src/lib.rs. That
file IS the crate's top module, so an item declared there is at
`crate::name` and needs no module of its own.
crate::election::ballots::count() = 12
A package with both files is two crates — a library and a binary
that uses it by name, not by path. That is why `use my_crate::…`
appears in main.rs and `use crate::…` inside lib.rs.
5. What decides where an item lives
Not the file. `pub` and the module tree decide visibility, and
the file layout is a convenience for humans on top of it. So the
question when splitting is never "which file is this in" but
"which module can see it" — and a 900-line file with three
`mod` blocks has exactly the same answer as three files.
Practice¶
Five paths to one function. Build a three-level module tree — election::ballots::spoiled and election::tally — and from four different places in it, name the same count() function: from a grandchild with super::, from the same grandchild absolutely, from a sibling module, and from main. All four should compile and return the same number.
Then write down the file layout that tree corresponds to, in both the modern and the mod.rs form. And answer the question the trap section raises: you add a fifth file, cargo build succeeds, and nothing in it runs — what is missing, and why is there no error message?
Solution
one_module_per_file_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: five paths to one function, from four vantage points.
//!
//! rustc --edition 2024 one_module_per_file_kata.rs -o /tmp/ompk && /tmp/ompk
mod election {
pub mod ballots {
pub fn count() -> u32 {
12
}
pub mod spoiled {
/// From two levels down: `super::super::` or `crate::`.
pub fn share() -> f64 {
let total = super::count();
let bad = 1;
f64::from(bad) / f64::from(total)
}
/// The same thing written absolutely.
pub fn share_absolute() -> f64 {
let total = crate::election::ballots::count();
f64::from(1u32) / f64::from(total)
}
}
}
pub mod tally {
/// A sibling module: up one, then down.
pub fn total() -> u32 {
super::ballots::count() * 5
}
/// `self::` is optional and occasionally load-bearing — it forces the
/// name to resolve as a module path rather than anything in scope.
pub fn total_twice() -> u32 {
self::total() * 2
}
}
}
use election::ballots::spoiled;
fn main() {
println!("1. The tree, as files");
println!(" src/main.rs mod election;");
println!(" src/election.rs mod ballots; mod tally;");
println!(" src/election/ballots.rs mod spoiled;");
println!(" src/election/ballots/spoiled.rs");
println!(" src/election/tally.rs");
println!(" Note ballots.rs is BOTH a module and a directory's parent. That");
println!(" is the whole 2018 change: a file beside a folder of the same");
println!(" name, instead of mod.rs inside it.");
println!();
println!("2. Four ways to name the same function");
println!(" from spoiled : super::count() = {}",
election::ballots::count());
println!(" from spoiled : crate::election::ballots::count() = {}",
election::ballots::count());
println!(" from tally : super::ballots::count() = {}",
election::ballots::count());
println!(" from main : election::ballots::count() = {}",
election::ballots::count());
println!(" All four resolve to one function. The path is relative to where");
println!(" you are STANDING in the module tree, and the tree came from the");
println!(" `mod` declarations rather than from the directories.");
println!();
println!("3. Running them");
println!(" spoiled::share() = {:.4}", spoiled::share());
println!(" spoiled::share_absolute() = {:.4}", spoiled::share_absolute());
println!(" election::tally::total() = {}", election::tally::total());
println!(" election::tally::total_twice() = {}", election::tally::total_twice());
println!();
println!("4. Which form to reach for");
println!(" crate:: when the item is far away, or the module might move.");
println!(" Absolute paths survive a rename of the module you are IN.");
println!(" super:: for a sibling, once. Two `super::super::` in a row is a");
println!(" sign the tree is wrong, not that you need a third.");
println!(" self:: rarely; it disambiguates a module path from a local name.");
println!(" a `use` when the same path appears more than twice in a file.");
println!();
println!("5. The failure that has no error message");
println!(" Add src/election/recount.rs and forget `mod recount;` in");
println!(" election.rs, and the file is simply not part of the crate. It is");
println!(" not compiled, so its syntax errors do not appear, its tests do");
println!(" not run, and nothing warns. `cargo build` succeeds. That is the");
println!(" most common \"why is my code not running\" in a new Rust project,");
println!(" and the answer is always the missing declaration.");
}
Verified output of one_module_per_file_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. The tree, as files
src/main.rs mod election;
src/election.rs mod ballots; mod tally;
src/election/ballots.rs mod spoiled;
src/election/ballots/spoiled.rs
src/election/tally.rs
Note ballots.rs is BOTH a module and a directory's parent. That
is the whole 2018 change: a file beside a folder of the same
name, instead of mod.rs inside it.
2. Four ways to name the same function
from spoiled : super::count() = 12
from spoiled : crate::election::ballots::count() = 12
from tally : super::ballots::count() = 12
from main : election::ballots::count() = 12
All four resolve to one function. The path is relative to where
you are STANDING in the module tree, and the tree came from the
`mod` declarations rather than from the directories.
3. Running them
spoiled::share() = 0.0833
spoiled::share_absolute() = 0.0833
election::tally::total() = 60
election::tally::total_twice() = 120
4. Which form to reach for
crate:: when the item is far away, or the module might move.
Absolute paths survive a rename of the module you are IN.
super:: for a sibling, once. Two `super::super::` in a row is a
sign the tree is wrong, not that you need a third.
self:: rarely; it disambiguates a module path from a local name.
a `use` when the same path appears more than twice in a file.
5. The failure that has no error message
Add src/election/recount.rs and forget `mod recount;` in
election.rs, and the file is simply not part of the crate. It is
not compiled, so its syntax errors do not appear, its tests do
not run, and nothing warns. `cargo build` succeeds. That is the
most common "why is my code not running" in a new Rust project,
and the answer is always the missing declaration.
See also¶
- Modules and visibility — what
pubdoes to the tree this page builds - Bringing names in with
use— shortening the paths above - Running a scratch program —
rustcalone,cargo new, and wheresrc/main.rscomes from - Scaffolding a practice tree — several crates in one workspace
Sources¶
File hierarchy ↗ and Crates ↗ in Rust by Example; the Reference's modules ↗ chapter for the file-resolution rules, including both layouts.
Po polsku¶
mod nazwa; to deklaracja, a nie import ani #include: mówi „istnieje moduł o tej nazwie, a jego treść leży w pliku obok”. Drzewo modułów bierze się więc z deklaracji mod, a nie z układu katalogów — dlatego election::tally::total() wygląda dokładnie tak samo, czy moduł siedzi w bloku mod { } w jednym pliku, czy w osobnym pliku, czy w podkatalogu, a przenoszenie go między tymi postaciami jest czystą refaktoryzacją, przy której nie rusza się ani jednego miejsca wywołania. Warto rozdzielić w głowie dwa słowa, które po polsku łatwo się zlewają: moduł jest jednostką języka, plik tylko wygodą dla człowieka.
Do zapamiętania są dwa układy plików. election.rs leżący obok katalogu election/ to postać z edycji 2018 i tę się stosuje; starsza trzyma zamiast tego election/mod.rs w katalogu, wciąż działa i spotka się ją w starszym kodzie — zmieniono ją dlatego, że w edytorze osiem otwartych kart o nazwie mod.rs niczego nie mówi. Ze ścieżkami reguła jest podobnie prosta: crate:: gdy element jest daleko albo moduł może się przenieść, bo ścieżka bezwzględna przeżyje zmianę nazwy modułu, w którym akurat stoisz; super:: do rodzeństwa, raz — podwójne super::super:: to sygnał, że drzewo jest źle zbudowane, a nie że potrzebujesz trzeciego. Korzeniem crate'a jest src/main.rs w programie i src/lib.rs w bibliotece, a pakiet z obydwoma plikami to dwa crate'y — stąd use my_crate::… w main.rs obok use crate::… w lib.rs.
Pułapka tej strony jest cicha i dlatego dotkliwa. Dodajesz src/election/recount.rs, zapominasz dopisać mod recount; w election.rs — i ten plik po prostu nie należy do crate'a. Nie jest kompilowany, więc jego błędy składniowe się nie pokazują, jego testy nie startują, nic nie ostrzega, a cargo build kończy się sukcesem. To najczęstsze „dlaczego mój nowy kod nic nie robi” w pierwszym projekcie w Ruscie. Jeśli przychodzisz z Pythona, zauważ, że sytuacja jest odwrócona: Python sam odnajduje moduły, przeszukując sys.path w czasie działania, i gdy czegoś brakuje, krzyczy ModuleNotFoundError — w Ruscie o pliku trzeba powiedzieć w czasie kompilacji, a objawem jest cisza. Błąd bliźniaczy, z drugiej strony, to E0428: zadeklarowanie tego samego modułu dwa razy jest podwójną definicją, a nie niewinnym „dołączone dwukrotnie” z nagłówka w C.
Szukaj po polsku: deklaracja modułu w Ruscie · moduły a pliki w Ruscie · rust mod declaration not include · rust new file not compiled missing mod · rust E0428 duplicate definition