Wonderfull Rust

The Module System

Rust's Module System

Rust's module system is a unified mechanism for organizing code, managing namespaces, and controlling visibility. Compared to other languages' module systems, its distinguishing features are private by default and explicit visibility level control.

Module Basics

The mod Keyword

Modules are defined with the mod keyword:

// Defining a module within the same file
mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn internal_helper() -> i32 {
        // Private: not accessible from outside this module
        42
    }
}

fn main() {
    let result = math::add(1, 2);
    // math::internal_helper();  // Compile error: private
}

File System and Modules

Rust's module system maps to the file system's directory structure:

src/
├── main.rs       (crate root)
├── math.rs       (math module)
└── network/
    ├── mod.rs    (network module)
    └── http.rs   (network::http submodule)
// src/main.rs
mod math;      // Loads src/math.rs
mod network;   // Loads src/network/mod.rs

fn main() {
    math::add(1, 2);
    network::http::get("https://example.com");
}

Since Rust 2018 edition, you can also represent modules by filename instead of mod.rs:

src/
├── main.rs
├── math.rs
└── network.rs      (network module)
└── network/
    └── http.rs     (network::http submodule)

Private by Default

The most important design decision in Rust's module system is that all items are private by default.

mod my_module {
    struct InternalData {
        value: i32,
    }

    fn internal_function() -> i32 {
        42
    }

    pub fn public_function() -> i32 {
        // Accessible from within the same module
        internal_function()
    }
}

In Java, the default is package-private. In Python, the convention is to use underscore prefixes to indicate private, but it's not enforced. In JavaScript (ESM), the default is module-private but you export to make things public.

Rust's approach is "only what you explicitly mark with pub is public" -- a design faithful to the principle of least privilege. This means:

  • Internal implementation changes don't break API compatibility: Anything without pub is guaranteed not to be used externally
  • Prevents accidental exposure: No risk of accidentally making internal functions public
  • Provides cues when reading code: Whether pub is present immediately tells you if an item is a public API or an internal implementation

Fine-Grained Visibility Control

Rust allows fine-grained control of visibility by adding modifiers to pub:

mod outer {
    pub mod inner {
        // Accessible from anywhere
        pub fn fully_public() {}

        // Accessible only from the parent module (outer)
        pub(super) fn parent_only() {}

        // Accessible only within the same crate
        pub(crate) fn crate_only() {}

        // Accessible only from a specific path
        pub(in crate::outer) fn specific_path() {}

        // Default: only within this module
        fn private() {}
    }

    fn test() {
        inner::fully_public();    // OK
        inner::parent_only();     // OK: this is the parent module
        inner::crate_only();      // OK: same crate
        inner::specific_path();   // OK: we're in crate::outer
        // inner::private();      // NG: private
    }
}
Visibility Accessible From
pub Anywhere (including external crates)
pub(crate) Within the same crate
pub(super) The parent module
pub(in path) Under the specified path
(none) Within the same module only

This graduated visibility control is especially useful in large projects. Use pub(crate) when "you don't want to expose it to external crates but want it freely available within the crate," and pub(super) when "it's a submodule's implementation detail but the parent module needs access."

use: Simplifying Paths

The use keyword introduces a module path into the current scope:

// Full path access
std::collections::HashMap::new();

// Introduced with use
use std::collections::HashMap;
HashMap::new();

Flexible use Syntax

// Import multiple items at once
use std::collections::{HashMap, HashSet, BTreeMap};

// Import the module itself
use std::collections;
let map = collections::HashMap::new();

// Merge nested paths
use std::io::{self, Read, Write};
// The above is equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;

// Glob import (commonly used in tests)
use std::collections::*;

// Alias
use std::collections::HashMap as Map;

use Placement: Top-Level and Local

use can be written not only at the top of files but also inside functions and blocks:

fn process() {
    use std::collections::HashMap;  // Only valid within this function

    let mut map = HashMap::new();
    map.insert("key", "value");
}

This flexibility lets you minimize namespace pollution while importing only what you need, where you need it.

Conventional use Style

The Rust community has conventions for how to write use:

// 1. Standard library
use std::collections::HashMap;
use std::io::{self, Write};

// 2. External crates
use serde::{Deserialize, Serialize};
use tokio::sync::Mutex;

// 3. Items from within the crate
use crate::config::Settings;
use crate::models::User;

cargo fmt automatically organizes this ordering for you.

Path Origins

Rust module paths have three starting points:

mod a {
    pub mod b {
        pub fn func() {}
    }

    pub fn example() {
        // Absolute path: from crate root
        crate::a::b::func();

        // Relative path: from current module
        b::func();

        // self: explicitly the current module
        self::b::func();

        // super: parent module
        // super::other_module::func();
    }
}
Origin Meaning
crate:: Absolute path from the crate root
self:: The current module
super:: The parent module
(none) Relative path from the current scope

Re-export: pub use

pub use re-exports an item:

// src/lib.rs
mod internal {
    pub mod deeply {
        pub mod nested {
            pub struct ImportantType;
        }
    }
}

// Hide internal structure while exposing via a convenient path
pub use internal::deeply::nested::ImportantType;

// Externally accessible as my_crate::ImportantType
// The internal module structure is hidden

pub use is an important tool for public API design. Even if you refactor internal module structure, the external API remains unchanged as long as you maintain the pub use paths.

The Relationship Between Crates and Modules

Rust's compilation unit is the crate:

  • Binary crate: An executable program with fn main()
  • Library crate: A library used by other crates
my_project/
├── Cargo.toml
├── src/
│   ├── main.rs    (binary crate root)
│   └── lib.rs     (library crate root)

A single package (the unit defined by Cargo.toml) can contain one library crate and multiple binary crates.

Summary

Design principles of Rust's module system:

  1. Private by default: All items are private by default. Explicitly publish with pub
  2. Graduated visibility levels: Fine-grained control with pub, pub(crate), pub(super), pub(in path)
  3. File system correspondence: Module structure maps to directory structure, making physical code layout match logical structure
  4. Flexible use syntax: Nested imports, aliases, local-scope use
  5. Re-export: pub use separates the API surface from internal structure

This design enforces the software engineering principle of "hiding internal implementation and intentionally controlling the public API" at the language level. In large projects, module boundaries naturally become API design boundaries, providing a foundation for maintainability and ease of refactoring.

Back to book