Skip to content
academia.sh

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.

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 mod declaration 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 pub keyword 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 single rustc call and form a single crate.
  • use is 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 not pub.

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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close