Module Organization

Okay, let’s dive into Rust’s module system, which is crucial for organizing larger projects and managing code complexity. We’ll cover the mod and use keywords, and how to structure your code into logical units.

Module Organization with mod and use in Rust

1. Welcome and Icebreaker:

Welcome back!

Icebreaker: If you were to organize your favorite physical space (like a room, a desk, or a workshop) into “modules,” what would those modules be, and what would each one contain?

2. Outline the Session:

Here’s the plan:

  1. Concept Explanation: We’ll introduce the concepts of modules, crates, and paths. We’ll cover how to define modules using mod, how to control visibility with pub, and how to bring items into scope with use. We’ll also touch on nested modules and file organization.
  2. Live Coding Demonstration: We’ll create a small project with multiple modules and demonstrate how to use mod and use to organize the code.
  3. Tasks: You’ll get exercises to practice creating and using modules in your own code.

Learning Outcomes: By the end of this session, you will be able to:

  • Understand the purpose of modules in Rust.
  • Define modules using the mod keyword.
  • Control the visibility of items within modules using pub.
  • Bring items into scope using the use keyword.
  • Create nested modules.
  • Organize modules into separate files.
  • Understand the relationship between crates, modules, and files.

Expectations: Module organization is a fundamental aspect of writing maintainable Rust code. Pay close attention to how mod and use work together, and how visibility is controlled.

3. Concept Explanation:

  • Crates: A crate is the top-level unit of compilation in Rust. It’s either a binary crate (an executable) or a library crate (code that can be used by other crates). Your Cargo.toml file defines a crate.
  • Modules: Modules are used to organize code within a crate. They provide a way to group related items (functions, structs, enums, etc.) and control their visibility.
  • Paths: Paths are used to refer to items (functions, structs, etc.) within modules. They are like file system paths, but for your code.

mod Keyword:

  • mod is used to define a module.
  • Module names follow the snake_case convention.
mod my_module {
    // Items within the module (functions, structs, etc.)
    fn my_function() {}
}

pub Keyword (Visibility):

  • By default, items within a module are private to that module. They can only be accessed from within the module itself.
  • The pub keyword makes an item public, meaning it can be accessed from outside the module.
mod my_module {
    pub fn my_public_function() {} // Accessible from outside the module
    fn my_private_function() {}     // Only accessible from within my_module
}

use Keyword:

  • use brings items from other modules into the current scope, so you can refer to them without using their full path.
mod my_module {
    pub fn my_function() {}
}

use crate::my_module::my_function; // Bring my_function into the current scope

fn main() {
    my_function(); // Now we can call it directly
}

Nested Modules:

Modules can be nested within other modules:

mod outer_module {
    pub mod inner_module {
        pub fn my_function() {}
    }
}

use crate::outer_module::inner_module::my_function;

fn main() {
    my_function();
}

File Organization:

  • src/main.rs: The entry point for a binary crate.
  • src/lib.rs: The entry point for a library crate.
  • Modules in Separate Files: You can define modules in separate files to keep your code organized.
    • mod my_module; (in src/main.rs or src/lib.rs or another module file) This tells Rust to look for a file named my_module.rs (in the same directory) or a directory named my_module with a file named mod.rs inside it.
      • my_module.rs (for simple modules)
      • my_module/mod.rs (for modules that contain submodules)

Example File Structure:

my_project/
├── Cargo.toml
└── src/
    ├── main.rs
    └── my_module.rs
    └── my_another_module/
        └── mod.rs

4. Live Coding Demonstration:

Let’s create a small library crate to demonstrate these concepts. We’ll start by creating a new project:

cargo new my_library --lib
cd my_library

Now open in your IDE.

src/lib.rs:

// src/lib.rs

pub mod math { // Define a public module named "math"
    pub fn add(a: i32, b: i32) -> i32 { // A public function within the "math" module
        a + b
    }

    fn subtract(a: i32, b: i32) -> i32 { // A private function (not accessible outside "math")
        a - b
    }
    pub mod advanced{ //Nested Module
        pub fn multiply(a:i32, b:i32) -> i32 {
            a * b
        }
    }
}

pub mod greetings; // Declare a module named "greetings" (defined in greetings.rs)

pub mod farewells { // Define a module named "farewells" inline.
    pub fn goodbye() -> String{
        "Good bye!".to_string()
    }
}

src/greetings.rs:

// src/greetings.rs

pub fn hello() -> String {
    "Hello, world!".to_string()
}

src/main.rs:

// src/main.rs
//use my_library::greetings::hello;
//use my_library::math::add;
//use my_library::farewells::goodbye;

use my_library::{greetings::*, math::*, farewells::goodbye}; // Import all public items from the module

fn main() {
    println!("{}", hello());
    println!("2 + 3 = {}", add(2, 3));
    println!("{}", goodbye());
    println!("{}", advanced::multiply(2, 3)); // Access nested module
}

Run this code using below command

cargo run

Key Takeaways:

  • mod: We define modules math, greetings, and farewells.
  • pub: We make math::add, greetings::hello, and farewells::goodbye public.
  • use: We bring functions into the main function’s scope using use. We show different ways to use use (specific imports, wildcard imports).
  • Separate Files: The greetings module is defined in greetings.rs.
  • Nested Modules: The advanced module is nested within the math module.
  • Inline Modules: The farewells module is defined directly within src/lib.rs.

5. Tasks

  1. Create a Geometry Module: Create a new library crate called geometry. Define a module named shapes within the crate. Inside the shapes module, define structs for Circle (with a radius field) and Rectangle (with width and height fields). Implement area methods for both structs. Make the structs and their area methods public. Write a main.rs that uses the geometry module.
  2. Nested Modules for a Game: Imagine you’re creating a game. Create a new library crate. Define the following modules:
    • characters: Contains structs for different character types (e.g., Player, Enemy).
    • items: Contains structs for different items (e.g., Weapon, Potion).
    • world: Contains modules for different areas of the game world (e.g., forest, desert, mountains). Each of these submodules should contain functions related to that area (e.g., explore, encounter_enemy). Make the necessary items public so that you could potentially use them from a main.rs file. You don’t need to fully implement the game logic, just focus on the module structure.
  3. Refactor Existing Code: Take one of your previous projects (e.g., the BankAccount or TodoList project) and refactor it to use modules. Move the struct definitions and their implementations into separate modules.
  4. Multiple Files: Create three file. main.rs, student.rs and teacher.rs. In student.rs create Student struct and impl blocks. In teacher.rs create Teacher struct and impl blocks. Use these structs and methods in main.rs
  5. Visibility: Create a module named my_module with public function, private function, public struct and private struct. And try to access all of these out of the module.