// #Rust
I remember a coworker hating on it, but I really love the simplicity of go’s visibility rules.
- A directory is a package and all identifiers defined in a package are visible to each other regardless of which file they are in. It is as if the files are all concatenated.
- Capitalized identifiers are exported out of the package, lowercase are private and only accessible within the package.
These simple rules are super easy to reason about. The first gives developers the flexibility to break things up and organize as they see fit without messin with visibility. And the simplicity of an on/off switch for exporting things is just…so great. That is the only power I want. For each package, I focus on what is external and what isn’t, doesn’t matter where the package is in a hierarchy.
Rust involves two layers of visibility rules. And a handful of ways to accomplish things. Plus more granular controls. All in all, it is easy to get turned around.
Already some complexity, two ways to make a module in rust.
Rust modules are comparable to go packages. They both organize code, control visibility, create namespaces. In go, there is only one way to create a package, a filesystem directory. In rust, a module needs to be explicitly declared with mod child_module1
and then can be implemented in one of three ways. The flexibility saddens me. A module is either inline (same file), a sibling file with the modules name, or a sibling directory with the module name. For a module to have child modules itself it needs a directory (so option #3). Options #1 and #2 are kinda nice for smaller modules.
I’d prefer just one way to do things, even if just the verbose option #3. But so it goes.
Let’s say you are coming from go land so you naturally make an internal module following option #3, a directory. It is a pretty complex module, so you start to bust up the initial mod.rs
into different files for organization purposes.
Following some go tendencies.
The difference from go is that these tiny modules also introduce a layer of visibility complexity, even if not desired. And rust has a more complex visibility system than go’s on/off switch. The rust book describes it best, and it boils down to these two rules.
- If an item is public, then it can be accessed externally from some module
m
if you can access all the item’s ancestor modules fromm
. You can also potentially be able to name the item through re-exports.- If an item is private, it may be accessed by the current module and its descendants.
In go, an item is marked public or private and that is the end of that. But in rust there is a “chain” element where the caller needs to have access to where the item lives. Importing an item is described by a path, crate::module1::module2::item
.
// Absolute path.
use crate::module_name::item_name;
// Relative path.
use self::item_name;
use super::module_name::item_name;
// Path from an external crate.
use std::collections::HashMap;
Explicit imports, the item name at the end is what ends up in the local namespace.
How do you know if a module has access to every module in a given path? There are two possibilities. First, if the module was declared as public, pub mod module_name
. Second, if the module is an ancestor of the current module. So the parent or grandparent or all the way up to the root of the crate.
That ancestor rule is special. The fact that the root crate
module is a shared ancestor of all modules in the crate can be useful. Also, a module has access to all items in an ancestor module, not just the exposed ones. This includes an ancestor’s direct, private, child modules.
Well wait…if tiny_module1
has visibility into its parent, internal_module
, and that gives it visibility to its child modules, does that mean tiny_module1
has access to its siblings like tiny_module2
? Turns out…yea! This matches my intuition and fits with rule #2 above.
mod parent {
mod sibling_1 {
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
mod child_1 {
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
}
pub mod child_2 {
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
}
}
mod sibling_2 {
pub fn add(left: u64, right: u64) -> u64 {
super::sibling_1::add(left, right)
}
mod child_1 {
pub fn add(left: u64, right: u64) -> u64 {
super::super::sibling_1::add(left, right)
}
}
}
}
A little kingdom of visibility.
So while there are a few more things to keep in mind, this simple go-like structure does kinda work in rust as well. But each tiny_module is its own module, so the item level visibility modifiers still apply. Where in go, all the files are essentially concatenated and can see each other’s private items, in rust only the public exported items are available between the modules. So more thought has to go into the module interface, but at least the path visibility isn’t an issue.
Privacy Firewall
Making a module private doesn’t hide the module itself, unlike an item in a module. It essentially gives that module the responsibility of dictating the exposed interface for every module under it.
Prelude
I think rust’s two rules of visibility naturally push a developer to make wide, flat module hierarchies. But if things do start to get a little deep, the prelude pattern might come in handy.
A few key points to the prelude.
- You stick the prelude module in the root module, or the top module it is trying to help summarize. The prelude module itself can be private since the use case is for descendant modules to pull it in.
- The prelude re-exports
pub
items which can then be dumped into a namespace withuse crate::prelude::*
. - The prelude module needs access to the things it is re-exporting, so it usually involves some
pub
module chains.
mod root {
// Private firewall.
mod sibling_1 {
pub mod child_1 {
pub mod grandchild_1 {
pub mod greatgrandchild_1 {
pub fn add_deep(left: u64, right: u64) -> u64 {
left + right
}
}
}
}
}
pub mod sibling_2 {
use super::prelude::*;
pub fn add_surface(left: u64, right: u64) -> u64 {
add_deep(left, right)
}
}
mod prelude {
pub use super::sibling_1::child_1::grandchild_1::greatgrandchild_1::add_deep;
}
}
A simple prelude.
The prelude is not cutting any corners, it can just help simplify common deep imports if they are used throughout a crate.
Granularity
There are a few other visibility descriptors which might complicate things, but the crux for all of these is that they too do not cut any corners. Any extra descriptors are further limiting visibility, defensive programming. They can be used in place of a normal pub
.
pub(crate)
// The calling modulem
must be in the same crate to access. The compiler will stop it from even being re-exported.pub(in path)
// The calling modulem
must be in the module of the given path, or a descendant of it. The path must be anchor’d bycrate
,self
, orsuper
.