Organizing Code with Modules, Packages, and Crates

Organizing Code with Modules, Packages, and Crates

Two loose threads finally tie together: the panic-vs-Result lesson's private value field inside Guess ('exactly how Rust draws and enforces that boundary is its own lesson'), and the Hello Cargo lesson's [package] section in Cargo.toml. The mod keyword for grouping code, Rust's default-private visibility and the pub keyword that opts out of it, use for shortening the paths you've been writing since std::collections::HashMap (and what use super::* in the testing lesson actually meant), splitting a module into its own file, and precise definitions of crate and package.

13 min read4 learning objectives

What You'll Learn

  • Use mod to group related items into a module, and access them with module::item path syntax — the same :: you've been writing since String::from and Vec::new
  • Explain Rust's default-private visibility rule, use pub to selectively expose structs, fields, functions, and methods, and connect this to Guess's private value field from the panic-vs-Result lesson
  • Use use to bring a path into scope, and explain what use super::* was doing inside the testing lesson's #[cfg(test)] mod tests block
  • Split a module out into its own file with mod name;, explain how the module tree mirrors the file tree, and define crate and package precisely against Cargo.toml from the Hello Cargo lesson

Two threads have been left dangling, on purpose, since early in this course. Back in the panic-vs-Result lesson, Guess had a field, value, with no pub in front of it — and the lesson promised that "exactly how Rust draws and enforces that boundary is its own lesson, a little further down the road." And all the way back in the Hello Cargo lesson, Cargo.toml opened with a section called [package] — a word that's been sitting there, unexplained, for twenty-eight lessons. Both threads end here, along with a third idea that ties them together: mod, the tool for grouping code into named units in the first place.

One module, this whole time

Here's the thing that made both of those threads invisible until now: privacy in Rust is a property of modules, not files or projects — and every single program in this course, so far, has been exactly one module. When everything lives in one module, every item can already see every other item, automatically, regardless of pub. So the pub in front of Guess, and in front of discounted_price back in the comments lesson, did nothing observable — there was no boundary yet for it to cross. The moment you introduce a second module, that changes completely, and pub stops being decoration.

mod — grouping related code

mod creates a module: a named box that groups related items together, with its own privacy boundary. Here's a small one, plus a main that uses it:

Walk through it slowly, because almost every piece is a callback:

  • mod inventory { ... } declares a module named inventory and gives it a body, right here in the same file.
  • inventory::Item::new(...) reaches into that module using :: — the exact same :: you've been writing since your very first String::from(...). It was never special syntax just for built-in types; :: is a path separator, full stop, and Type::function has been a path into a module the entire time — you just hadn't met a hand-written one yet.
  • pub struct Item, pub fn new, pub fn quantity, pub fn restock — every one of these is invisible outside mod inventory without pub. Remove any single one, and the corresponding line in main stops compiling.
  • And here's the field-level version of the exact same rule, twice, on one struct: pub name: String can be read directly as item.name from main — but quantity: i32, with no pub, cannot. The only way to read it is through .quantity(), and the only way to change it is through .restock(). This is exactly the shape of Guess's private value field and its .value() getter — and now you can see precisely why it worked: Guess and the code using it were always implicitly in the same module, so the boundary was invisible. Put Guess in its own module, and that same private field becomes truly unreachable from outside it — on purpose.

One more recognition, while you're here: the testing lesson's #[cfg(test)] mod tests { ... } was already a mod — a module nested inside the same file, exactly like inventory above. Everything in this lesson was already true there. #[cfg(test)] just adds one more rule on top: "and also, only compile this module when running tests."

use — shortening paths you already write

Typing inventory::Item::new(...) every time gets old fast. use brings a path into scope, so you can refer to its last segment alone:

use inventory::Item; doesn't copy or duplicate anything — Item and inventory::Item are the exact same type, both before and after this line. All use does, from this point in the file downward, is let you write the short name instead of the full path. Look back at every use you've already written, and they're all this same one idea:

  • use std::collections::HashMap;, since the hash-maps lesson HashMap lives in the standard library's collections module, isn't part of the prelude, so every file that wants the short name HashMap has to ask for it.
  • use std::fs;, since the propagating-errors lesson — this brings the module fs itself into scope, which is why the calls read fs::read_to_string(...) rather than just read_to_string(...). That's a common, deliberate choice: writing the module name at the call site makes it obvious the function isn't defined locally.
  • use super::*;, inside #[cfg(test)] mod testssuper is a special path segment meaning "the module containing this one." For mod tests, that's the rest of the file. And * is a glob: "everything at this path," all at once, instead of naming each item individually. Put together, use super::*; means exactly what it always quietly meant: "bring everything from the file surrounding this test module into scope, by its short names."

One module, many files

Everything so far has kept inventory in the same file as main. That stops being realistic the moment a module grows — and the fix is almost suspiciously simple. Replace the body of mod inventory { ... } with a semicolon — mod inventory; — and Rust goes looking for the module's code in its own file instead:

src/inventory.rs contains exactly what used to sit between the braces of mod inventory { ... } — nothing more, nothing less. The file boundary becomes the module boundary, so the wrapper itself is no longer written:

And main.rs shrinks down to one declaration plus the code that actually does something:

Same program, same output, two files instead of one. mod inventory; says "there is a module called inventory; find its contents in inventory.rs." Everything about pub, privacy, and use from the last two sections carries over completely unchanged — modules behave identically whether their body sits between braces in the same file or in a file of their own.

Crates and packages

Zoom out one more level, and two words from the Hello Cargo lesson finally get precise definitions. A crate is the smallest amount of code the compiler considers at one time — either a binary crate (has a main function, produces an executable) or a library crate (no main, meant to be used by other code). A package is one or more crates plus the Cargo.toml that describes them. Every cargo new you've run in this course created a package containing exactly one binary crate, rooted at src/main.rs — which is why Cargo.toml's very first section has always been called [package]: it's naming the package, and the name and version it gives apply to the crate inside. The module tree you just built is rooted at that same file:

mod inventory; added inventory as a child of the crate root in this tree — and that tree is exactly what :: paths like inventory::Item are addresses into. A package can hold a library crate too, rooted at src/lib.rs, with its own module tree alongside the binary — the shape that lets one project both run as a program and be used as a dependency by someone else's Cargo.toml.

Quick exercise

  1. Go back to Guess, from the panic-vs-Result lesson. Wrap the whole thing — the struct and its impl block — in a module of your own naming, say mod guessing_game { ... }. In main, either write guessing_game::Guess::new(42) or add use guessing_game::Guess; and write Guess::new(42). Notice what you didn't have to change: not one pub, not the private value field, nothing inside impl Guess at all. Guess was written correctly for module boundaries twenty-eight lessons before you knew modules existed.
  2. Pick any function from an earlier lesson — largest, from the generics lesson, is a good choice. Move it into a mod of your choosing, mark it pub, and call it from main through the module path or a use. Then, just to feel the boundary for yourself, delete the pub and run cargo check — and watch it refuse the call from main that worked one second ago.

Step back and look at what just got renamed, not replaced. mod is the testing lesson's mod tests, seen in full. pub is the reason Guess's value field could be private at all — a promise made twenty-eight lessons ago, kept. use is use std::collections::HashMap, use std::fs, and use super::*, all explained at once. :: is every Type::function call you've written since String::from, revealed as a path through a module tree. And crate / package are Cargo.toml's [package] section, named precisely for the first time. Nothing here was new machinery — it was vocabulary for things you'd been doing correctly, by following examples, since the first lesson that gave you a struct with a pub fn new.

Every program in this course has read its data from somewhere inside the source code itself — a vec![...] written by hand, a hardcoded "count.txt". Real programs take their input from outside themselves: arguments typed on the command line, environment variables, files whose names aren't known until the moment the program runs. The next lesson is where that finally happens — reading real input with std::env and std::fs, using exactly the module-path fluency you now have, and the start of a new stretch of this course where "does it compile" gives way to "build something a person could actually run": files and I/O.