Skip to content

Latest commit

 

History

History
335 lines (238 loc) · 7.81 KB

README.md

File metadata and controls

335 lines (238 loc) · 7.81 KB

Managing Growing PRojects with Packages, Crates, and Modules1

To run the program:

$ cargo run --bin growing-projects
   Compiling growing-projects v0.1.0 ...

Lessons learned

The programs I wrote so far have been one module, in one file.

As a project grows, I organize code by:

  • Splitting it into multiple modules
  • And then multiple files.

A package can contain multiple binary cartes and optionally one library crate.

As a package grows, I extract parts into separate crates that become external dependencies.

NOTE: For very large projects comprising a set of interrelated packages that evolve together, Cargo also provides workspaces, but that isn't covered until Chapter 14.

In general, the features that are supported to organize code include:

  1. Packages: A Cargo feature that lets you build, test, and share crates.
  2. Crates: A tree of modules that produces a libray or executable.
  3. Modules and use: Let you control the organization, scope, privacy of paths.
  4. Paths: A way of naming an item, such as a struct, function, or module.

Packages and Crates

Crates

A crate is the smallest amount of code that the Rust compiler considers at a time.

For example if you run:

rustc main.rust

... the compiler considers that single file to be a crate.

Crates can contain modules, and modules may be defined in other files.

A crate comes in one of two forms: a binary crate or a library crate:

  • Binary crates are programs that are compiled and run, i.e. a CLI or server.
    • Each must have a function called main.
  • Library crates don't have a main, and don't compile to an executable.
    • Instead, they define functionality shared with multiple projects.

NOTE: In practice, the word crate often means library crate.

Packages

A package is a bundle of one or more crates that provide a set of functionality. A package contains a Cargo.toml2 file that describes how to build those crates. Cargo is a package that contains the binary crate for the command-line tool I've been using up until this point.

NOTE: Cargo also provides a library crate that other packages can use!

When you create a new package, i.e. with cargo new, which creates a:

  • Cargo.toml configuration file.
  • A crate root file, either src/main.rs or src/lib.rs.
$ cargo new my-project
    Created binary (application) `my-project` package

$ ls my-project
Cargo.toml
src

# If cargo new my-project --lib, this file would be main.lib
$ ls my-project/src
main.rs

NOTE: Remember that crates can only contain a single library crate.

However multiple binary creates are allowed by creating a src/bin:

src/
  bin/
    a.rs
    b.rs

Defining Modules to Control Scope and Privacy

The module system is roughly:

  • Paths allow you to name items
  • The use keyword brings a path into scope
  • The pub keyword makes items public

The typical workflow is:

  • The compiler starts at the root crate's crate root (typically src/{lib,main}.rs).

  • In the crate root, you can declare new modules. Let's say the garden module:

    • It will look inline (in the crate root) for:

      mod garden {
        // ...
      }

      or (these examples are semantically the same):

      mod garden;
      
      // ...
    • If not found, it will look in src/garden.rs.

    • If not found, it will look in src/garden/mod.rs.

  • In any other file (other than crate root), declare submodules, i.e. vegetables:

    • Inline, directly following mod vegetables; or within mod vegetables {}.
    • If not found, in the file src/garden/vegetables.rs.
    • If not found, in the ffile src/garden/vegetables/mod.rs.
  • Once a module is part of your crate, and it's visible to you, you refer to it:

    // For example, an `Asparagus` type in the garden/vegetables module:
    crate::garden::vegetables::Asparagus
  • Code within a module is private by default.

    • To make a module public, declare it with pub mod instead of mod.
    • To make an item within a public module public, use pub, i.e. pub enum.
  • Using the use keyword to create scope shortcuts:

    use crate::garden::vegetables::Asparagus;
    
    // You can now refer directly to `Asparagus`.
backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

To write a library crate that provides functionality of a restaurant:

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}
crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

Paths for Referring to an Item in the Module Tree

To navigate the module tree (similar to a filesystem), we need a path:

  • An absolute path is the full path starting from a crate root

    • For an external crate, it's {name}::....
    • For the current create, it's crate::....
  • A relative path starts from the current module and uses either:

    • self
    • super (similar to ../ in a filesystem path)
    • an identifier in the current module

The preference in the Rust Book is to:

  • Use absolute paths to make refactors easy.

  • Import the module (not the full path) to call functions:

    // BAD.
    use crate::front_of_house::hosting::add_to_waitlist {
      pub fn eat_at_restaurant() {
        add_to_waitlist();
      }
    }
    
    // GOOD.
    use crate::front_of_house::hosting {
      pub fn eat_at_restaurant() {
        hosting::add_to_waitlist();
      }
    }
  • Use full names, i.e. front_of_house.rs over front_of_house/mod.rs.

  • Import the full path to use structs, enums, and other items:

    // GOOD.
    use std::collections::HashMap;
    
    fn main() {
      let mut map = HashMap::new();
      map.insert(1, 2);
    }
  • As an exception, use the module name or as rename if names would conflict:

    // GOOD.
    use std::fmt;
    use std::io;
    
    fn f1() -> fmt::Result { /* ... */ }
    fn f2() -> io::Result<()> { /* ... */ }
    
    // GOOD.
    use std::fmt::Result;
    use std::io::Result as IoResult;
    
    fn f1() -> Result { /* ... */ }
    fn f2() -> IoResult<()> { /* ... */ }

Can also use nested paths to clean-up large use lists:

use std::cmp::Ordering;
use std::io;

// Semantically the same:
use std::{cmp::Ordering, io};

Another example:

use std::io;
use std::io::Write;

// Semanticaly the same:
use std::io::{self, Write};

There is also a glob operator, which is typically used by a tests module:

// See also: https://doc.rust-lang.org/std/prelude/index.html#other-preludes
use std::collections::*;

Can also write pub use to re-export code (the book will expand in chapter 14).

Using External Packages

For example, using rand (https://crates.io/crates/rand):

cargo add rand
[dependencies]
rand = "0.8.5"
use rand::Rng;

fn main() {
  let secret_number = rand::thread_rng().gen_range(1..=100);
}

Notes

Though not specifically covered by this section, some cargo CLI steps:

# Resolves and adds the "rand" package (from crates.io) to "growing-projects".
cargo add rand -p growing-projects

Footnotes

  1. Source: https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html

  2. Why this file starts with a uppercase character, just to throw off my file naming schemes, you can read about here.