Rust knows two types of code units: crates and modules. A crate is an external library, complete with its own Cargo.toml configuration file, dependencies, tests, and code. Modules, on the other hand, split the crate into logical parts that are only visible to the user if they import specific functions. Since the 2018 edition of Rust, the difference in using these structural encapsulations has been minimized.
Splitting your code with crates and modules
Getting ready
This time, we are going to create two projects: one that offers some type of function and another one to use it. Therefore, use cargo to create both projects: cargo new rust-pilib --lib and cargo new pi-estimator. The second command creates a binary executable so we can run the compilation result, while the former is a library (crate).
This recipe is going to create a small program that prints out estimations of pi () and rounds them to two decimal places. It's nothing fancy and easy for anyone to understand.
How to do it...
In just a few steps, we will be working with different modules:
- First, we are going to implement the rust-pilib crate. As a simple example, it estimates the constant pi using the Monte Carlo method. This method is somewhat similar to throwing darts at a dartboard and counting the hits. Read more on Wikipedia (https://en.wikipedia.org/wiki/Monte_Carlo_method). Add to the tests submodule this snippet:
use rand::prelude::*;
pub fn monte_carlo_pi(iterations: usize) -> f32 {
let mut inside_circle = 0;
for _ in 0..iterations {
// generate two random coordinates between 0 and 1
let x: f32 = random::<f32>();
let y: f32 = random::<f32>();
// calculate the circular distance from 0, 0
if x.powi(2) + y.powi(2) <= 1_f32 {
// if it's within the circle, increase the count
inside_circle += 1;
}
}
// return the ratio of 4 times the hits to the total
iterations
(4_f32 * inside_circle as f32) / iterations as f32
}
- Additionally, the Monte Carlo method uses a random number generator. Since Rust doesn't come with one in its standard library, an external crate is required! Modify Cargo.toml of the rust-pilib project to add the dependency:
[dependencies]
rand = "^0.5"
- As good engineers, we are also going to add tests to our new library. Replace the original test module with the following tests to approximate pi using the Monte Carlo method:
#[cfg(test)]
mod tests {
// import the parent crate's functions
use super::*;
fn is_reasonably_pi(pi: f32) -> bool {
pi >= 3_f32 && pi <= 4.5_f32
}
#[test]
fn test_monte_carlo_pi_1() {
let pi = monte_carlo_pi(1);
assert!(pi == 0_f32 || pi == 4_f32);
}
#[test]
fn test_monte_carlo_pi_500() {
let pi = monte_carlo_pi(500);
assert!(is_reasonably_pi(pi));
}
We can even go beyond 500 iterations:
#[test]
fn test_monte_carlo_pi_1000() {
let pi = monte_carlo_pi(1000);
assert!(is_reasonably_pi(pi));
}
#[test]
fn test_monte_carlo_pi_5000() {
let pi = monte_carlo_pi(5000);
assert!(is_reasonably_pi(pi));
}
}
- Next, let's run the tests so we are certain of the quality of our product. Run cargo test in the root of the rust-pilib project. The output should be somewhat like this:
$ cargo test
Compiling libc v0.2.50
Compiling rand_core v0.4.0
Compiling rand_core v0.3.1
Compiling rand v0.5.6
Compiling rust-pilib v0.1.0 (Rust-Cookbook/Chapter01/rust-pilib)
Finished dev [unoptimized + debuginfo] target(s) in 3.78s
Running target/debug/deps/rust_pilib-d47d917c08b39638
running 4 tests
test tests::test_monte_carlo_pi_1 ... ok
test tests::test_monte_carlo_pi_500 ... ok
test tests::test_monte_carlo_pi_1000 ... ok
test tests::test_monte_carlo_pi_5000 ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests rust-pilib
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
- Now we want to offer the crate's feature(s) to the user, which is why we created a second project for the user to execute. Here, we declare to use the other library as an external crate first. Add the following to Cargo.toml in the pi-estimator project:
[dependencies]
rust-pilib = { path = '../rust-pilib', version = '*'}
- Then, let's take a look at the src/main.rs file. Rust looks there to find a main function to run and, by default, it simply prints Hello, World! to standard output. Let's replace that with a function call:
// import from the module above
use printer::pretty_print_pi_approx;
fn main() {
pretty_print_pi_approx(100_000);
}
- Now, where does this new function live? It has its own module:
// Rust will also accept if you implement it right away
mod printer {
// import a function from an external crate (no more extern
declaration required!)
use rust_pilib::monte_carlo_pi;
// internal crates can always be imported using the crate
// prefix
use crate::rounding::round;
pub fn pretty_print_pi_approx(iterations: usize) {
let pi = monte_carlo_pi(iterations);
let places: usize = 2;
println!("Pi is ~ {} and rounded to {} places {}", pi,
places, round(pi, places));
}
}
- This module was implemented inline, which is common for tests—but works almost like it was its own file. Looking at the use statements, we are still missing a module, however: rounding. Create a file in the same directory as main.rs and name it rounding.rs. Add this public function and its test to the file:
pub fn round(nr: f32, places: usize) -> f32 {
let multiplier = 10_f32.powi(places as i32);
(nr * multiplier + 0.5).floor() / multiplier
}
#[cfg(test)]
mod tests {
use super::round;
#[test]
fn round_positive() {
assert_eq!(round(3.123456, 2), 3.12);
assert_eq!(round(3.123456, 4), 3.1235);
assert_eq!(round(3.999999, 2), 4.0);
assert_eq!(round(3.0, 2), 3.0);
assert_eq!(round(9.99999, 2), 10.0);
assert_eq!(round(0_f32, 2), 0_f32);
}
#[test]
fn round_negative() {
assert_eq!(round(-3.123456, 2), -3.12);
assert_eq!(round(-3.123456, 4), -3.1235);
assert_eq!(round(-3.999999, 2), -4.0);
assert_eq!(round(-3.0, 2), -3.0);
assert_eq!(round(-9.99999, 2), -10.0);
}
}
- So far, the module is ignored by the compiler since it was never declared. Let's do just that and add two lines at the top of main.rs:
// declare the module by its file name
mod rounding;
- Lastly, we want to see whether everything worked. cd into the root directory of the pi-estimator project and run cargo run. The output should look similar to this (note that the library crate and dependencies are actually built with pi-estimator):
$ cargo run
Compiling libc v0.2.50
Compiling rand_core v0.4.0
Compiling rand_core v0.3.1
Compiling rand v0.5.6
Compiling rust-pilib v0.1.0 (Rust-Cookbook/Chapter01/rust-pilib)
Compiling pi-estimator v0.1.0 (Rust-Cookbook/Chapter01/pi-
estimator)
Finished dev [unoptimized + debuginfo] target(s) in 4.17s
Running `target/debug/pi-estimator`
Pi is ~ 3.13848 and rounded to 2 places 3.14
- Library crates are not the only ones to have tests. Run cargo test to execute the tests in the new pi-estimator project:
$ cargo test
Compiling pi-estimator v0.1.0 (Rust-Cookbook/Chapter01/pi-
estimator)
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running target/debug/deps/pi_estimator-1c0d8d523fadde02
running 2 tests
test rounding::tests::round_negative ... ok
test rounding::tests::round_positive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Now, let's go behind the scenes to understand the code better.
How it works...
In this recipe, we explored the relationship between crates and modules. Rust supports several ways of encapsulating code into units, and the 2018 edition has made it a lot easier to do. Seasoned Rust programmers will miss the extern crate declaration(s) at the top of the files, which is nowadays only necessary in special cases. Instead, the crate's contents can be used right away in a use statement.
In this way, the line between modules and crates is now blurred. However, modules are much simpler to create since they are part of the project and only need to be declared in the root module to be compiled. This declaration is done using the mod statement, which also supports implementation in its body—something that is used a lot in testing. Regardless of the implementation's location, using an external or internal function requires a use statement, often prefixed with crate:: to hint toward its location.
Alternatively to simple files, a module can also be a directory that contains at least a mod.rs file. This way, large code bases can nest and structure their traits and structs accordingly.
A note on function visibility: Rust's default parameter is module visibility. Hence, a function declared and implemented in a module can only be seen from within that module. Contrary to that, the pub modifier exports the function to outside users. The same goes for properties and functions attached to a struct.
We've successfully learned how to split our code with crates and modules. Now, let's move on to the next recipe.