Introduction
Rust’s module system provides a powerful way to structure code while enforcing strict visibility rules. Unlike some languages where files within the same directory automatically share visibility, Rust requires explicit declarations for visibility. This ensures encapsulation and prevents accidental dependencies between modules. Understanding how visibility works is crucial for designing maintainable, modular, and well-structured Rust programs.
Lateral Visibility
In Rust, modules within the same directory are not automatically visible to each other. Each module must explicitly declare its relationship with others using mod declarations or use statements. This behavior ensures that a module does not accidentally expose internals that should remain private.
When multiple modules reside in the same directory, there are two common ways to structure them:
-
Declaring dependencies explicitly within each module:
- If
mod_a.rs,mod_b.rs, andmod_c.rsexist within a parent folder, you can declaremod_binsidemod_aand depend on it, and similarly declaremod_binsidemod_cif needed. However, this can lead to tight coupling and unnecessary dependencies.
- If
-
Using a
mod.rsto centralize declarations:- Instead of declaring each module’s dependencies within their respective files, you can create a
mod.rsfile inside the parent directory and declaremod a; mod b; mod c;. This allows each module to refer to others usingsuper::a,super::b, orsuper::c, making dependencies explicit without tightly coupling them.
- Instead of declaring each module’s dependencies within their respective files, you can create a
Choosing between these approaches depends on the desired encapsulation level. The mod.rs approach is generally preferable for cleaner, more maintainable code structures.
Private Structs and Associated Traits
Marking a struct as pub determines whether it can be used outside its defining module, but struct fields remain private unless explicitly marked pub. This distinction allows encapsulation while still making the struct type itself available for external use.
mod library {
pub struct PublicStruct {
pub field: i32, // Explicitly public
private_field: i32, // Implicitly private
}
}A key principle in Rust is that if a struct is private, its trait implementations are also private. Even if the trait itself is public, the trait implementation for a private struct is inaccessible outside its module. This enforces encapsulation and prevents leaking implementation details.
mod library {
pub trait PublicTrait {
fn do_something(&self);
}
struct PrivateStruct;
impl PublicTrait for PrivateStruct {
fn do_something(&self) {
println!("This works internally");
}
}
}Even though PublicTrait is accessible, external code cannot use it with PrivateStruct because PrivateStruct is not visible outside library. This is an intentional design choice in Rust to maintain encapsulation while allowing internal modularity.
Public Modules with Private Contents
Declaring a module as pub does not mean everything inside it is accessible. By default, items inside a pub mod remain private unless explicitly marked pub. This is a powerful way to expose an API boundary while keeping implementation details hidden.
pub mod api {
fn internal_logic() {}
pub fn exposed_function() {
internal_logic();
}
}External code can call api::exposed_function(), but internal_logic() remains inaccessible. This is useful for enforcing encapsulation while exposing only necessary interfaces.
Best Practices
- Encapsulate implementation details: Keep modules private when they contain internal logic that should not be directly accessed.
- Use
mod.rsfor clarity: When working with multiple related modules, define them inmod.rsand usesuper::references to avoid unnecessary coupling. - Be mindful of trait visibility: If a struct is private, its trait implementations are also private, enforcing controlled access to implementation details.
- Use
pub(crate)when needed: If something should be accessible within the crate but not externally,pub(crate)is a good choice.
A root cause for Rust’s strict visibility rules stems from its emphasis on safety and explicitness. Unlike languages that allow deep introspection or implicit access through package conventions, Rust enforces visibility constraints at the language level, reducing unintended dependencies and improving maintainability.