Sam Pagenkopf
Posted on December 19, 2017
Do you remember the first time you made a project with more than one source file? Was it with headers, with classes...or, with modules?
Modules have certain traits. They allow various items to coexist in a unit, without being bound to a class. A module file generally has its interface (outward-facing public parts) on top, and its implementation (dirty inner details) on the bottom.
Understanding Rust's modules comes from understanding the keywords mod
, pub
, and use
. To state them simply:
-
mod
declares a module. -
pub
exposes. -
use
pulls things in.
Let's begin with an example. Modules can be defined two ways. First in place:
// main.rs
mod food {
pub fn eat() {
println!("Good.");
}
}
fn main() {
food::eat();
}
Second, as a separate file:
// main.rs
mod food;
fn main() {
food::eat();
}
// food.rs or food/mod.rs
pub fn eat() {
println!("Yum.");
}
So more specifically:
mod X
means: let there be a module X, defined either here, in braces, or in a separate file namedX.rs
orX/mod.rs
.
pub fn eat
makes eat
visible to main. Without pub
, main would not be able to call eat
.
Note that without mod food
in main, Rust would ignore food.rs
entirely.
Next, let's expand the above to something more deeply nested:
// main.rs
mod food {
pub mod lunch {
pub fn eat() {
println!("Take a break.");
}
}
}
fn main() {
food::lunch::eat();
}
Or, as multiple files. Note the use of mod.rs
, a special filename:
// main.rs
mod food;
fn main() {
food::lunch::eat();
}
// food/mod.rs
pub mod lunch;
// food/lunch.rs
pub fn eat() {
println!("Hamburger.");
}
Does this look like you expected? Notice how both fn eat
and mod lunch
are made pub
. They must be carried up this way, one level at a time.
And of course, anything without pub
is invisible outside its module.
Perhaps surprisingly, pub
treats struct members and methods on an individual basis.
// food.rs
struct Meal {
pub taste: String,
price: u32, // cannot see price
}
impl Meal {
pub fn eat(&self) {
println!("I taste {}.", self.taste);
}
fn purchase(&self) { // cannot call purchase directly
println!("Spent ${}. Ouch.", self.price);
}
}
Having a private variable price
means that you cannot make a Meal
outside of food
. A constructor method can help with that.
// food.rs
impl Meal {
pub fn fancy() -> Self {
Meal {
taste: String::from("miracles on plate"),
price: 44100,
}
}
}
// main.rs
mod food;
fn main() {
let lunch = food::Meal::fancy();
lunch.eat();
}
So, to recap:
pub
exposes an item, either on module or struct level, to its surrounding level.
Do note that Meal::fancy
could also be named Meal::new
, or even other things.
Next, let's examine use
to complete our set of three. use
is a tool that pulls in words to reduce visual clutter and finger work.
mod food;
use food::Meal;
fn main() {
let lunch = Meal::fancy();
lunch.eat();
}
Or even:
mod food;
use food::Meal::fancy;
fn main() {
let lunch = fancy();
lunch.eat();
}
Pulling in fancy
directly is an extreme example, but it does work. In the same way that pub
can apply to structs, so can use
.
A wrinkle with use
is that it has absolute paths. To understand paths, consider this non-working example:
mod food {
fn cheaper(a: &Meal, b: &Meal) -> u32 {
std::cmp::min(a.price, b.price)
}
}
This fails because std::cmp
is the same as writing self::std::cmp
, self
here being food
. It's a relative path. So, let's use an absolute path by adding ::
to the start.
mod food {
fn cheaper(a: &Meal, b: &Meal) -> u32 {
::std::cmp::min(a.price, b.price)
}
}
This works, but the use
keyword is also an absolute path, and can reduce typing in the long run.
mod food {
use std;
fn cheaper(a: &Meal, b: &Meal) -> u32 {
std::cmp::min(a.cost, b.cost)
}
}
Here's another way of doing it. Yes, use
does respect its scope.
mod food {
fn cheaper(a: &Meal, b: &Meal) -> u32 {
use std::cmp::min;
min(a.cost, b.cost)
}
}
There is some light alternate syntax for use
as well, in order to pull in multiple things.
mod food {
pub mod bread {
pub struct Slice {}
pub struct Loaf {}
}
pub struct Plate {}
pub struct Napkin {}
}
use food::bread::*; // pulls in Slice and Loaf
use food::{Plate, Napkin};
So, to be specific:
use
brings module or struct items into the current scope from an absolute path.
Also note that pub use
can be used as a flattening method.
mod food {
pub mod breakfast {
pub mod cereal {
pub fn eat() {
println!("Snip, crankle, porp.");
}
}
}
pub use self::breakfast::cereal;
}
fn main() {
food::cereal::eat();
}
This enables you to divide into further modules for organization, without adding a burden to the outside world.
Overall, it is better to be more explicit (avoid too much *
) and use
judiciously. It's better to know where things are coming from when you see them later, but added visual clutter and typing are no fun.
To recap:
-
mod
declares a module. -
pub
exposes an item by a single level. -
use
brings things from an absolute path to the current scope.
There is more, but I hope I have helped get things started. Let me know if you're interested in a second part.
Posted on December 19, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.