5

Easily Understand Rust Modules Across Multiple Files With This Guide

 1 year ago
source link: https://hackernoon.com/easily-understand-rust-modules-across-multiple-files-with-this-guide
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

TL;DR

  • šŸ’”Ā Easy and simple explanation of Rust's modules across different files.
  • šŸ¤æĀ We'll deep dive into a real-world example to explore the module system.
  • šŸ“ˆĀ Tons of diagrams to help you understand.

The module system in Rust can be confusing for developers coming from other languages. It took me a while to understand it so I want to share with you how it works and how to organize your program across multiple files with ease.

Let's go.

Rust Modules Across Files

Rust requires the developers to manually build module trees. The way to do it is by declaring modules with theĀ modĀ keyword.

The module tree starts with the crate root, usuallyĀ src/lib.rsĀ for a library crate orĀ src/main.rsĀ for a binary crate. The Rust compiler will first look in the crate root for modules to compile.

Let's say you want to import a module "a" in a binary crate, you can declare the module like this:

main.rs

mod a;

fn main() { /* do amazing things */ }

The compiler will look for the module in theĀ srcĀ directory in the following places:

InĀ src/a.rs

.
ā”œā”€ā”€ Cargo.lock
ā”œā”€ā”€ Cargo.toml
ā””ā”€ā”€ src
    ā”œā”€ā”€ a.rs
    ā””ā”€ā”€ main.rs

Or inĀ src/a/mod.rs

.
ā”œā”€ā”€ Cargo.lock
ā”œā”€ā”€ Cargo.toml
ā””ā”€ā”€ src
    ā”œā”€ā”€ a
    ā”‚   ā””ā”€ā”€ mod.rs
    ā””ā”€ā”€ main.rs

By declaringĀ mod aĀ inĀ main.rs, you've built a module tree like this:

Initial Rust module tree

Rust Submodules Across Files

Within a module, you can create submodules to further organize your code. Let's say you want to declare module "b" and "c" in module "a":

/src/a/mod.rs

mod b;
mod c;

The compiler will look for the submodules inĀ src/aĀ directory:

.
ā”œā”€ā”€ Cargo.lock
ā”œā”€ā”€ Cargo.toml
ā””ā”€ā”€ src
    ā”œā”€ā”€ a
    ā”‚   ā”œā”€ā”€ b.rs
    ā”‚   ā”œā”€ā”€ c.rs
    ā”‚   ā””ā”€ā”€ mod.rs
    ā””ā”€ā”€ main.rs

Now you've build a tree like this:

Rust module tree with submodules

Visibility with "pub"

By default, all theĀ itemsĀ in a module are private. They are only visible by the items in the same module.

src/a/mod.rs

mod b;
mod c;

fn do_a() {} // only the other functions in module a can use it
             // it's not visible to main.rs

In order for its parent modules to have access to the functionĀ do_a, we need to add the key wordĀ pub.

src/a/mod.rs

pub fn do_a() {} // now it's visible to main.rs

We can accessĀ do_aĀ using theĀ path qualifierĀ ::.

src/main.rs

mod a;

fn main() {
    a::do_a();
}

We can use the same pattern for submodules.

src/a/b.rs

pub fn do_b() {} // visible to module "a" and all the submodules of module "a"

By addingĀ pubĀ toĀ do_b, the function now is accessible to module "a".

src/a/mod.rs

mod b;
mod c;

pub fn do_a {
    b::do_b();
}

do_bĀ is also accessible to the submodules of module "c". You can access it with either the absolute or relative path.

src/a/c.rs

pub fn do_c {
    crate::a::b::do_b(); // absolute path
    super::b::do_b(); // relative path
}

Re-exporting Items

An item of a submodule is not accessible to a non-parent module. For example, we can try to accessĀ do_bĀ inĀ main.rs

src/main.rs

mod:a;

fn main() {
    a::b::do_b();
    // ^^^^ function `do_b` is private
}

You'll see an error message sayingĀ do_bĀ is private. That's becauseĀ do_bĀ is only accessible within module "a" so far. To make it visible to the crate root, We need to re-export it by addingĀ pubĀ to the module "b" declaration from module "a".

src/a/mod.rs

pub mod b;
// --snip--

The "use" Declaration

TheĀ useĀ declaration can help you shorten the path when accessing an item in another module. For example, we can refactor the module "a":

src/a/mod.rs

mod b;
mod c;

use b::do_b;
use c::do_c;

pub fn do_a {
    do_b();
    do_c();
}

It creates a local name binding to its path forĀ do_bĀ andĀ do_c.Ā useĀ is very useful for long paths.

A Real World Example

To demonstrate the Rust's module system, I created a simple CLI calledĀ affme, short for "affirm me".

affme demo

affmeĀ is an self-affirmation generator. The CLI takes in a name as a parameter and displays a randomized affirmation.

The demo isĀ available on GitHub. Feel free to take a look at the repo and try it outāœØ

The code design is straightforward:

affme code design

In the "format" block,

  • It takes a user input,
  • concatenates the input with a random affirmation and a random emoji,
  • applies a random font color to the concatenated affirmation,
  • and finally outputs the affirmation.

To showcase the module system across files, I design the module tree as following:

affme module tree design

A few things worth mentioning:

  • This package has two crates, one binary and one library. I use the library crate to encapsulate the implementation and the binary crate to execute the CLI.
  • In the library crate rootĀ src/lib.rs, it accesses functions from theĀ affirmationĀ andĀ formattermodule.
  • TheĀ affirmationĀ module and both of the submodules in theĀ formatterĀ module are using the same function in theĀ randomĀ module to randomly pick an item. Because theĀ affirmationmodule andĀ formatterĀ submodules are in different branches of the tree, we need to declare theĀ randomĀ module in the common ancestor of the module tree.

In the file system, it looks like this:

.
ā”œā”€ā”€ Cargo.lock
ā”œā”€ā”€ Cargo.toml
ā”œā”€ā”€ src
ā”‚   ā”œā”€ā”€ affirmation.rs
ā”‚   ā”œā”€ā”€ formatter
ā”‚   ā”‚   ā”œā”€ā”€ color.rs
ā”‚   ā”‚   ā”œā”€ā”€ emoji.rs
ā”‚   ā”‚   ā””ā”€ā”€ mod.rs
ā”‚   ā”œā”€ā”€ lib.rs
ā”‚   ā”œā”€ā”€ main.rs
ā”‚   ā””ā”€ā”€ random.rs
ā””ā”€ā”€ target

Let's dive into the library crate root to see how the code is structured.

src/lib.rs

mod affirmation;
mod formatter;
mod random;

use affirmation::Affirmation;
use formatter::format;

pub fn affirm(name: &str) -> String {
    let affirmation = Affirmation::new().random();
    format(affirmation, name)
}

Here you can see the module declarations on the top. You can also find theĀ useĀ declarations to create the local name binding forĀ AffirmationĀ andĀ format.

The random module is straightforward:

src/random.rs

use rand::Rng;

pub fn pick<'a, T: ?Sized>(items: &[&'a T]) -> &'a T {
    let random_index: usize = rand::thread_rng().gen_range(0..items.len());
    items.get(random_index).unwrap()
}

It has a publicĀ pickĀ function that returns a random item from an array slice. I use the function to pick random affirmations, emojis, and colors. Let's take a look atĀ affirmationĀ module as an example:

src/affirmation.rs

use crate::random;

#[derive(Debug)]
pub struct Affirmation<'a> {
    affirmations: [&'a str; 6],
}

impl<'a> Affirmation<'a> {
    pub fn new() -> Self {
        let affirmations = [
            "You're beautiful",
            "You're awesome",
            "You're wonderful",
            "You've got this",
            "You can do all things",
            "Go get it",
        ];
        Affirmation { affirmations }
    }

    pub fn random(&self) -> &'a str {
        random::pick(&self.affirmations)
    }
}

You can see theĀ useĀ declaration for theĀ randomĀ module. TheĀ affirmationĀ module is able to access theĀ randomĀ module because theĀ randomĀ module was declared in the library crate root. I use theĀ pubĀ keyword on theĀ AffirmationĀ struct and its functions so that the crate root has visibility over them.

You can find the same coding pattern in theĀ emojiĀ andĀ colorĀ submodule.

To bring it all together, let's take a look at theĀ formatĀ module.

src/formatter/mod.rs

mod color;
mod emoji;

use color::Color;
use colored::*;
use emoji::Emoji;

pub fn format(affirmation: &str, name: &str) -> String {
    let emoji = Emoji::new();
    let color = Color::new();

    let phrase = format!("{}, {} {}", affirmation, name, emoji.random())
        .color(color.random())
        .bold()
        .to_string();

    format!(
        "{}\n{}\n{}\n{}\n{}",
        "*".repeat(phrase.len() + 2).magenta(),
        format!("*{}*", " ".repeat(phrase.len())).magenta(),
        format!("    āœļø  ...{}  ", phrase,),
        format!("*{}*", " ".repeat(phrase.len())).magenta(),
        "*".repeat(phrase.len() + 2).magenta()
    )
}

It bringsĀ colorĀ andĀ emojiĀ submodules in scope so we can concatenate the full affirmation with random emoji and random font color.

Final Thoughts

Rust Modules across multiple files is a little different from other languages but once you understandĀ mod,Ā use, andĀ pub, the module design becomes easier and intentional.

Rust Module Cheat Sheet

  • A module tree starts from the crate root.
  • UseĀ modĀ to build your tree with modules and submodules.
  • UseĀ pubĀ to make module items visible to the parent module.
  • You can re-export withĀ pub modĀ orĀ pub use.

References


This article was originally posted on Daw-Chihā€™s website.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK