Chapter 6: Functions, Modules, and Crates
Functions organise behaviour, modules organise code, and crates organise entire libraries or programs. Rust encourages clean boundaries between these layers so projects stay readable and easy to maintain. This chapter walks through each layer in turn so you can understand how Rust structures real software.
Functions and return values
Functions group work into reusable pieces. They take inputs, perform actions and return values. Rust functions are explicit about what they use and what they produce, which makes their behaviour predictable.
Defining basic functions
A function declaration lists the name, parameters and return type. The last expression becomes the return value unless you include an explicit return.
fn add(a: i32, b: i32) -> i32 {
a + b
}
println!("{}", add(3, 4));
Type annotations for parameters and return types are required so the compiler knows exactly how the function is meant to behave.
Returning multiple values with tuples
Rust functions can return a single value, but that value may be a tuple if you want to group several results.
fn split(n: i32) -> (i32, i32) {
(n / 2, n % 2)
}
let (q, r) = split(9);
println!("{q}, {r}");
This pattern avoids creating temporary structs when you only need a simple bundle of data.
Early returns
You can end a function early with return. This may help with error handling or special cases.
fn safe_div(a: i32, b: i32) -> Option {
if b == 0 {
return None;
}
Some(a / b)
}
The function either returns a division result or indicates absence with None.
return only when it makes the flow clearer.
Visibility and modules
Modules group related items and control what is visible from the outside. By default everything in a module is private to that module unless you mark it pub. This helps you hide internal details while exposing a clean public surface.
Declaring modules
You declare a module with mod. Its contents may sit inline or in another file that shares the module’s name.
mod util {
pub fn shout(msg: &str) {
println!("{msg}!");
}
}
util::shout("hi");
Inside a module you control which items become part of its public API by adding pub.
Nested modules
Modules may contain submodules. This allows deep organisation when a project grows.
mod net {
pub mod http {
pub fn get() {
println!("fetching");
}
}
}
net::http::get();
The module tree mirrors your directory structure when modules are split across files.
pub item is visible to the outside world only if every module above it is also public in the path being used.
Crates and packages
A crate is the unit of compilation in Rust. It may be a library or a binary program. A package is a folder that contains one or more crates along with a Cargo.toml file. Most packages contain a single crate, but the system allows more when needed.
Library and binary crates
A library crate exposes an API and has no main function. A binary crate defines fn main() and produces an executable.
// src/lib.rs
pub fn greet() {
println!("hi");
}
// src/main.rs
use mycrate::greet;
fn main() {
greet();
}
This separation helps you share functionality between projects or publish reusable components.
The role of Cargo.toml
Cargo.toml describes the package: its name, version, dependencies and crate type. Cargo uses this file to build, test and distribute your project.
[package]
name = "demo"
version = "0.1.0"
[dependencies]
rand = "0.8"
Changes in Cargo.toml guide the build system and affect how your crate is compiled.
Use paths and re-exporting
The use keyword shortens long paths to keep code readable. Re-exporting with pub use lets your crate present a simpler external interface by exposing internal items under new paths.
Using paths
You bring names into scope with use. This avoids repeating full module paths.
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("x", 1);
This keeps code concise and keeps the focus on logic instead of paths.
Re-exporting with pub use
pub use re-exports an internal item as part of your public API. This helps you reorganise internal modules while presenting a clean surface.
mod math {
pub fn square(n: i32) -> i32 { n * n }
}
pub use math::square;
println!("{}", square(5));
External users of your crate can now call square directly without knowing the module layout.
Cargo features and profiles
Cargo offers tools to customise how your crate is built. Features switch optional parts of your code on or off. Profiles control optimisation, debug info and general build behaviour.
Defining features
Features are declared in Cargo.toml. They let users opt in to additional capabilities without forcing them on everyone.
[features]
serde = ["serde_crate"]
fast = []
Code that depends on a feature usually sits behind #[cfg(feature = "name")] attributes.
Build profiles
Cargo includes profiles such as dev, release and test. Each profile changes optimisation levels, debug data and build speed.
[profile.release]
opt-level = 3
debug = false
Release builds focus on runtime performance; dev builds focus on quick iteration with helpful debug information.