Skip to content

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

election::summary()
election::tally::total()

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.path at run time; Rust is told about them at compile time by mod, 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__.py is election.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 mod is 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 with Package Check as Server on is pub(crate) for its contents, and a package interface is the list of what it exports — which is exactly pub on a mod. What does not transfer is the sequencing: an INCLUDE really does splice text at that point, so an include used twice compiles twice; mod x; names a module once, and a second declaration is E0428.
  • C / C++. mod is 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

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