Getting Started with Rust Using Rustlings — Part 9: Modules

Jesse Verbruggen
7 min readFeb 28, 2023
Part 9: Modules

Welcome to Part 9 of my miniseries on learning Rust with Rustlings! So far, we’ve covered a wide range of topics in Rust, including variables, functions, structs, enums, ownership, and strings. In this article, we will dive into the world of Rust’s module system.

If you want to read the previous article, you can get to it by following the link below.

In this series, I am going over my process of learning the syntax of Rust with Rustlings. In Part One, I explain how to install Rust and Rustlings. So if you haven’t yet done that, click the link below.

The Module System

As a powerful feature of Rust, the module system enables you to organize your code into logical groups for better readability, maintainability, and reuse. But what exactly are modules, and how do they work in Rust?

In this article, we’ll dive into Rust’s module system basics, including how to define and use modules, import and re-export items, and control visibility and privacy with access modifiers. Whether you’re a beginner or a seasoned Rust developer, understanding Rust’s module system is essential for writing efficient, modular, and scalable code. So, let’s get started!

Packages and Crates

Binary Crates

When it comes to creating crates in Rust, there are two types you should be aware of: binary and library crates. As the name suggests, binary crates can be compiled into an executable file that can be run as a standalone program, whether a server or a command-line tool. These crates require a main function that defines what should happen when the executable is executed. So far in this series, the crates we’ve created have all been binary crates.

Library Crates

On the other hand, library crates are designed to be used by other projects and can’t be compiled into an executable file. Instead, they exist to provide functionality that can be shared across different projects. For example, the rand crate is a popular Rust library crate that provides functionality for generating random numbers.

Whenever a “crate” is mentioned in Rust development, we usually talk about a library crate, as this is the popular programming concept of libraries.

Crate Root

Finally, it’s worth noting that the crate root is an important concept in Rust. This source file serves as the Rust compiler's starting point and forms your crate's root module. When compiling a crate, the compiler first looks in the crate root file for code to compile. Typically this is src/lib.rs for library crates and src/main.rs for binary crates.

Package

A package is a grouping of one or more crates in the Rust programming language that cooperate to provide functionality. A package can include numerous binary crates combined to create executable files. A package, however, is limited to containing a single library crate that offers shared functionality for other applications. It’s important to remember that every package must have one crate, whether a library crate or a binary crate.

Modules

Once again, to demonstrate how modules work, I will use the example of my cat. So let’s assume we have a crate with the following directory structure:

app/
├── Cargo.toml
├── src/
│ ├── pets/
| ├───── cats.rs
| ├───── mod.rs
│ └── main.rs

File: src/pets/cats.rs

#[derive(Debug)]
pub struct Cat {
pub name: String,
pub age: u8,
pub color: String,
}

File: src/pets/mod.rs

pub mod cats;

File: src/main.rs

pub mod pets;

use pets::cats::Cat;

fn main() {
let cleo = Cat {
name: String::from("Cleo"),
color: String::from("Grey"),
age: 2,
};
println!("{:?}", cleo);
}

In our crate root, we declare the modules we want to use in our application. In this case, we use pub mod pets to tell the compiler to look for a file at either src/pets.rs or in this case src/pets/mod.rs. In that file, we define all submodules of pets. To import the Cat struct, we’ve made all of its fields accessible with pub and the struct itself as well. We then import the submodule Struct using pets::cats::Cat. Now in main.rs we can create a variable that holds a Cat struct. The output of our program looks like this.

Cat { name: "Cleo", age: 2, color: "Grey" }

And just like that, you now understand how to use modules to separate our code into logical parts using multiple files and group them in folders with submodules.

Modules cheatsheet

Here’s a quick cheat sheet on modules in Rust:

Start from the crate root: When compiling a crate, the compiler first looks in the crate root file (src/lib.rs for a library crate or src/main.rs for a binary crate) for code to compile.

Declaring modules: In the crate root file or any file, you can declare new modules with mod <module_name>;. The compiler will look for the module’s code in these places:

  • Inline, within curly brackets that replace the semicolon following mod <module_name>
  • In the file <module_name>.rs
  • In the file <module_name>/mod.rs

Paths to code in modules: Once a module is part of your crate, you can refer to code in that module from anywhere else in that same crate, as long as the privacy rules allow, using the path to the code.

Private vs public: Code within a module is private from its parent modules by default. To make a module public, use the pub keyword. To make items within a public module public as well, use pub before their declarations.

The use keyword: Within a scope, the use keyword creates shortcuts to items to reduce the repetition of long paths. In any scope that can refer to crate::<module_name>::<item_name>, you can create a shortcut with use crate::<module_name>::<item_name>; and from then on you only need to write <item_name> to make use of that item in the scope.

Exercises

modules1.rs

mod sausage_factory {
// Don't let anybody outside of this module see this!
fn get_secret_recipe() -> String {
String::from("Ginger")
}

fn make_sausage() {
get_secret_recipe();
println!("sausage!");
}
}

fn main() {
sausage_factory::make_sausage();
}

In the sausage_factory module, which we can access in this file because it’s declared in the same file, we want to access make_sausage. To do this, we need to add the pub keyword to make it accessible.

mod sausage_factory {
// Don't let anybody outside of this module see this!
fn get_secret_recipe() -> String {
String::from("Ginger")
}

pub fn make_sausage() {
get_secret_recipe();
println!("sausage!");
}
}

fn main() {
sausage_factory::make_sausage();
}

With that, we can access this function and solve our first problem.

modules2.rs

mod delicious_snacks {
// TODO: Fix these use statements
use self::fruits::PEAR as ???
use self::veggies::CUCUMBER as ???

mod fruits {
pub const PEAR: &'static str = "Pear";
pub const APPLE: &'static str = "Apple";
}

mod veggies {
pub const CUCUMBER: &'static str = "Cucumber";
pub const CARROT: &'static str = "Carrot";
}
}

fn main() {
println!(
"favorite snacks: {} and {}",
delicious_snacks::fruit,
delicious_snacks::veggie
);
}

Using the as keyword, we can create a shortcut to PEAR and CUCUMBER and give them the name that is used in the main function. In addition to this, we need to make these public by adding pub in front of the use keyword.

mod delicious_snacks {
pub use self::fruits::PEAR as fruit;
pub use self::veggies::CUCUMBER as veggie;

mod fruits {
pub const PEAR: &'static str = "Pear";
pub const APPLE: &'static str = "Apple";
}

mod veggies {
pub const CUCUMBER: &'static str = "Cucumber";
pub const CARROT: &'static str = "Carrot";
}
}

fn main() {
println!(
"favorite snacks: {} and {}",
delicious_snacks::fruit,
delicious_snacks::veggie
);
}

modules3.rs

use ???

fn main() {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => println!("1970-01-01 00:00:00 UTC was {} seconds ago!", n.as_secs()),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
}
}

You can use the ‘use’ keyword to bring module paths from modules from anywhere and especially from the Rust standard library into your scope. Bring SystemTime and UNIX_EPOCH from the std::time module. Bonus style points if you can do it with one line!

The use keyword allows us to import multiple modules at the same time using brackets {ModuleOne, ModuleTwo}. To finish this exercise in style, let’s do just that and import both SystemTime and UNIX_EPOCH at once.

use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(n) => println!("1970-01-01 00:00:00 UTC was {} seconds ago!", n.as_secs()),
Err(_) => panic!("SystemTime before UNIX EPOCH!"),
}
}

I hope this article helped you understand the basics of Rust’s module system and how it can be used to organize your code in a logical and scalable way. With modules, you can control scope and privacy, reuse code, and reduce the repetition of long paths. By following the rules outlined in this article, you can write efficient and modular code that’s easy to read, maintain, and share with others.

The next part about HashMaps is available at the link below. Here, we learn how to use Rust’s HashMaps and discover what makes HashMaps unique and how to work with them efficiently.

If I helped you learn more about Rust or you found this interesting, please clap 👏 and share this article to help more people find these articles.

Let’s help each other learn and grow! Peace out✌️

--

--