Functions, Modules and Crates
Functions with typed parameters and tail expressions, modules and privacy, use paths, and how packages and crates organize a Rust program across files and libraries.
Functions
A function is declared with fn, a name in
snake_case, a parameter list in which every parameter
has a type, and, after ->, the type of the result.
The order of functions in a file does not matter: main
can call a function that is defined further down, without a forward
declaration or a header.
fn main() {
let cents = cost_cents(350, 2700);
println!("350 kWh at €0.2700/kWh: {cents} cents");
}
// Price in ten-thousandths of a euro per kWh; result in cents.
fn cost_cents(kwh: u64, price_per_kwh: u64) -> u64 {
kwh * price_per_kwh / 100
}
350 kWh at €0.2700/kWh: 9450 cents
The money is kept in whole numbers, as on Variables, Types and Mutability: the price €0.2700 is 2700 ten-thousandths of a euro, and 350 × 2700 / 100 is 9450 cents. Parameter types are never inferred, even where the compiler could guess them. That is deliberate: the signature is a contract that callers and the compiler can check without reading the body, and an error inside one function cannot change what other functions are allowed to pass in. Inside the body, local variables are inferred as usual (see type inference).
Tail expressions and return
Rust is expression-oriented (see
Control Flow and Pattern Matching): a block { … } has a value, the value of its last
expression. A
tail expression is that last
expression of a block or function body, written without a semicolon;
its value becomes the value of the block, and for a function body,
the return value. kwh * price_per_kwh / 100
above is one. A semicolon turns an expression into a statement and
throws its value away, so one stray semicolon changes what the
function returns:
fn cost_cents(kwh: u64, price_per_kwh: u64) -> u64 {
kwh * price_per_kwh / 100;
}
fn main() {
println!("{}", cost_cents(350, 2700));
}
example.rs:1:48: error[E0308]: mismatched types: expected `u64`, found `()`
The full message explains what happened and how to fix it:
error[E0308]: mismatched types
--> example.rs:1:48
|
1 | fn cost_cents(kwh: u64, price_per_kwh: u64) -> u64 {
| ---------- ^^^ expected `u64`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | kwh * price_per_kwh / 100;
| - help: remove this semicolon to return this value
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.
(), pronounced "unit", is the type with exactly one
value, also written (). It is what a block without a
tail expression produces, and the return type of a function that
declares none: fn log_line(…) means
fn log_line(…) -> (). It plays the role of
void in C and of Python's implicit None,
but it is an ordinary value with a type.
Use return to leave a function before its end, for
example after checking a special case. To return several values,
return a tuple and take it apart with let:
fn euros_and_cents(cents: u64) -> (u64, u64) {
(cents / 100, cents % 100)
}
fn describe(kwh: u64) -> String {
if kwh == 0 {
return String::from("no usage");
}
let (euros, cents) = euros_and_cents(kwh * 2700 / 100);
format!("{kwh} kWh, €{euros}.{cents:02}")
}
fn log_line(meter: &str, kwh: u64) {
println!("{meter}: {}", describe(kwh));
}
fn main() {
log_line("M-30004", 350);
log_line("M-30007", 0);
let nothing: () = log_line("M-30011", 12);
println!("log_line returned {nothing:?}");
}
M-30004: 350 kWh, €94.50 M-30007: no usage M-30011: 12 kWh, €3.24 log_line returned ()
log_line takes the meter as &str, a
borrowed piece of text, rather than an owned String: it
only needs to read it. Whether a parameter takes a value or borrows
it is one of the most important choices in a Rust signature,
explained on Ownership and
Borrowing and References.
Modules and privacy
A
module is a named container for
functions, types, constants and other modules, declared with
mod. Modules form a tree inside a crate, and each
module is a privacy boundary: an item is private to the module it is
defined in (and that module's children) unless it is marked
pub. A private helper is an implementation detail that
the rest of the program cannot call, so you can change it without
breaking anything outside the module.
mod tariff {
/// Fixed price in ten-thousandths of a euro per kWh (€0.2700).
pub const FIXED_PRICE: u64 = 2700;
pub fn cost_cents(kwh: u64) -> u64 {
round_to_cents(kwh * FIXED_PRICE)
}
fn round_to_cents(ten_thousandths: u64) -> u64 {
(ten_thousandths + 50) / 100
}
}
fn main() {
println!("price: {} ten-thousandths of a euro per kWh", tariff::FIXED_PRICE);
println!("350 kWh: {} cents", tariff::cost_cents(350));
println!("rounded: {} cents", tariff::round_to_cents(945_049));
}
example.rs:17:43: error[E0603]: function `round_to_cents` is private: private function
main may use FIXED_PRICE and
cost_cents, but not round_to_cents; the
error also points at the private definition. Remove the last line
and the program prints the price and 9450 cents. The privacy check
happens at compile time, so it costs nothing when the program runs.
Visibility comes in a few steps:
| Written as | Visible in |
|---|---|
| (nothing) | the module itself and its child modules |
pub(super) |
also the parent module |
pub(crate) |
the whole crate, but not other crates that use it |
pub |
everywhere the module itself can be reached, including other crates when the path is public all the way down |
Privacy also applies to the fields of a struct: a
pub struct Meter with a private field can only be
created and changed through the functions its module offers, which
lets the module guarantee rules such as "a reading never goes down".
That is how Rust does encapsulation; structs and their methods are
on
Structs, Enums and Methods.
Paths: crate, self and super
Items in modules are named by paths with :: between the
parts, like folders in a file path. A path can start in three
places:
-
crate::starts at the root of the current crate (an absolute path, like/in a file system); -
self::or just a name starts in the current module; -
super::starts in the parent module, like...
mod billing {
pub mod tariff {
pub fn price(kind: &str) -> u64 {
match kind {
"fixed" => 2700,
_ => super::VARIABLE_PRICE,
}
}
}
pub mod invoice {
use super::tariff;
pub fn total_cents(kind: &str, kwh: u64) -> u64 {
(kwh * tariff::price(kind) + 50) / 100
}
}
const VARIABLE_PRICE: u64 = 2600; // the variable tariff, €0.2600
}
use billing::invoice;
fn main() {
println!("fixed: {} cents", invoice::total_cents("fixed", 350));
println!("other: {} cents", crate::billing::invoice::total_cents("variable", 350));
println!("price: {}", billing::tariff::price("fixed"));
}
fixed: 9450 cents other: 9100 cents price: 2700
Two details are worth noticing. tariff reads its
parent's private constant VARIABLE_PRICE through
super::: a child module can see everything in its
ancestors, but not the other way round. And for main to
reach billing::tariff::price, both the module
tariff and the function must be pub; a
public function in a private module is still unreachable from
outside. Both prices are Kestrel's tariffs on this site: €0.2700
fixed and €0.2600 variable.
use declarations
A
use declaration creates a
short local name for an item that lives at a longer path, for the
rest of the module it appears in. It does not load, copy or include
anything; the compiler already knows every item of the crate and of
its dependencies. Braces import several items from one place, and
as gives an item another name, which helps when two
imports would clash:
use std::collections::BTreeMap;
mod tariff {
pub const FIXED_PRICE: u64 = 2700;
pub fn cost_cents(kwh: u64) -> u64 {
(kwh * FIXED_PRICE + 50) / 100
}
}
use tariff::{FIXED_PRICE, cost_cents as fixed_cost};
fn main() {
let mut usage = BTreeMap::new();
usage.insert("h0042", 412);
usage.insert("h0017", 350);
println!("price {FIXED_PRICE}");
for (home, kwh) in &usage {
println!("{home}: {} cents", fixed_cost(*kwh));
}
let mut homes = Vec::new(); // Vec and String need no use: they are in the prelude
for home in usage.keys() {
homes.push(home.to_string());
}
println!("{homes:?}");
}
price 2700 h0017: 9450 cents h0042: 11124 cents ["h0017", "h0042"]
The standard library is the crate std, and every Rust
program can use it without declaring a dependency. A small part of
it, the prelude, is imported into every module
automatically: Vec, String,
Option with Some and None,
Result with Ok and Err,
Box, common traits such as Clone, and
since Rust 1.80 the function size_of. Everything else
needs a path or a use. Forget it, and the compiler
usually suggests the right line:
fn main() {
let mut usage = BTreeMap::new();
usage.insert("h0017", 350);
println!("{usage:?}");
}
example.rs:2:21: error[E0433]: cannot find type `BTreeMap` in this scope: use of undeclared type `BTreeMap`
help: consider importing this struct | 1 + use std::collections::BTreeMap; |
By convention, functions are imported through their module (use crate::tariff;
and then tariff::cost_cents(…)), so a call shows where
the function comes from, while types are imported by their own name
(use std::collections::BTreeMap;). A
pub use re-exports an item: a library can keep its
internal module layout and still offer a short public path such as
kestrel_readings::Reading.
Modules in separate files
So far every module was written inline, in braces. In a real project
each module usually has its own file. The line
mod tariff;, with a semicolon instead of a body, tells
the compiler to load the module's contents from
tariff.rs next to the file that declares it. The files
of a Cargo project live in src/; the next example
creates a project with the
cargo command,
writes two files and runs the program:
cargo new -q --vcs none kestrel-readings
cd kestrel-readings
cat > src/tariff.rs <<'EOF'
/// Fixed price in ten-thousandths of a euro per kWh (€0.2700).
pub const FIXED_PRICE: u64 = 2700;
pub fn cost_cents(kwh: u64) -> u64 {
(kwh * FIXED_PRICE + 50) / 100
}
EOF
cat > src/main.rs <<'EOF'
mod tariff;
fn main() {
let cents = tariff::cost_cents(350);
println!("350 kWh: €{}.{:02}", cents / 100, cents % 100);
}
EOF
cargo run
Compiling kestrel-readings v0.1.0 (/home/amara/kestrel-readings)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
Running `target/debug/kestrel-readings`
350 kWh: €94.50
The file tariff.rs contains the module's body only: no
mod tariff { … } around it. A file is never a module on
its own; it becomes one because a mod line somewhere
declares it, and a file that no mod line mentions is
simply not compiled. Submodules go into a folder named after the
parent: mod rates; inside tariff.rs loads
src/tariff/rates.rs. An older layout puts the parent
module in src/tariff/mod.rs instead of
src/tariff.rs; both work, and you will meet both.
kestrel-readings/
├── Cargo.toml
└── src/
├── main.rs crate root: mod tariff;
├── tariff.rs the module tariff: mod rates;
└── tariff/
└── rates.rs the module tariff::rates
Crates and packages
A
crate is the unit the Rust compiler
builds in one go: one tree of modules, starting at a crate root
file, compiled into one program or one library. rustc never sees
individual modules separately; it gets the crate root and follows
the mod lines from there. There are two kinds:
-
A
binary crate compiles to an
executable program. It must have a
mainfunction; its root in a Cargo project issrc/main.rs. -
A
library crate has no
main; it offers itspubitems to other crates. Its root issrc/lib.rs, and Cargo compiles it into an.rlibfile that only the Rust compiler reads. When Rust programmers say "a crate" without more context, they usually mean a library, such as one from crates.io.
A Cargo package is a folder with a
Cargo.toml that describes how to build one or more
crates: at most one library crate and any number of binary crates. A
common layout puts the real work in the library and keeps
main.rs small, so that tests and other programs can use
the same code. Inside the binary, the library is used under the
package's name, with hyphens replaced by underscores:
cargo new -q --vcs none kestrel-readings
cd kestrel-readings
cat > src/tariff.rs <<'EOF'
/// Fixed price in ten-thousandths of a euro per kWh (€0.2700).
pub const FIXED_PRICE: u64 = 2700;
pub fn cost_cents(kwh: u64) -> u64 {
(kwh * FIXED_PRICE + 50) / 100
}
EOF
cat > src/lib.rs <<'EOF'
//! Shared code for Kestrel's reading tools.
pub mod tariff;
/// Formats an amount in cents as euros: 9450 -> "€94.50".
pub fn euros(cents: u64) -> String {
format!("€{}.{:02}", cents / 100, cents % 100)
}
EOF
cat > src/main.rs <<'EOF'
use kestrel_readings::{euros, tariff};
fn main() {
println!("350 kWh: {}", euros(tariff::cost_cents(350)));
}
EOF
cargo build -q
ls target/debug
./target/debug/kestrel-readings
build deps examples incremental kestrel-readings kestrel-readings.d libkestrel_readings.d libkestrel_readings.rlib 350 kWh: €94.50
One package, two crates: libkestrel_readings.rlib is
the library crate, kestrel-readings the program. The
module tariff now hangs under lib.rs, and
main.rs reaches it only through the library's public
items, exactly as an outside user would. The -q option
keeps Cargo quiet. Comments that start with /// and
//! are documentation comments; they are covered with
cargo doc on
Testing, Docs and Tooling.
Path dependencies
Code shared between several projects goes into its own library
package, and the projects that need it list it under
[dependencies] in their Cargo.toml. Most
dependencies come from the public registry crates.io, which is where
supply-chain questions start (see
The Crate Ecosystem and Supply-Chain Safety). A path dependency points to a folder on disk instead:
nothing is downloaded, which makes it the normal choice for packages
that live in the same repository.
In this example a developer on Kestrel's platform team keeps the
CRC-16/ARC checksum of P1 telegrams (explained on
Smart Meters and the P1 Port) in a small library package, p1crc, and uses it from
a command-line tool:
[package]
name = "readings-cli"
version = "0.1.0"
edition = "2024"
[dependencies]
p1crc = { path = "../p1crc" }
cargo new -q --vcs none --lib p1crc
cat > p1crc/src/lib.rs <<'EOF'
//! CRC-16/ARC, the checksum at the end of a P1 telegram.
/// Reflected polynomial 0xA001, start value 0, no final XOR.
pub fn crc16_arc(data: &[u8]) -> u16 {
let mut crc: u16 = 0;
for &byte in data {
crc ^= u16::from(byte);
for _ in 0..8 {
if crc & 1 != 0 {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}
crc
}
EOF
cargo new -q --vcs none readings-cli
cd readings-cli
echo 'p1crc = { path = "../p1crc" }' >> Cargo.toml
cat > src/main.rs <<'EOF'
fn main() {
let crc = p1crc::crc16_arc(b"123456789");
println!("check value: {crc:04X}");
}
EOF
cargo generate-lockfile -q
cat Cargo.lock
cargo run
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "p1crc"
version = "0.1.0"
[[package]]
name = "readings-cli"
version = "0.1.0"
dependencies = [
"p1crc",
]
Compiling p1crc v0.1.0 (/home/amara/p1crc)
Compiling readings-cli v0.1.0 (/home/amara/readings-cli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/readings-cli`
check value: BB3D
cargo new ends every Cargo.toml with an
empty [dependencies] section, so the
echo line appends the dependency in the right place,
giving the file shown above. The program calls
p1crc::crc16_arc by its full path, without a
use: a dependency's name is available as the start of a
path in every module. It prints BB3D, the published check value of
CRC-16/ARC. Cargo compiled the dependency first, and recorded both
packages in Cargo.lock, the file that pins the exact
versions of everything a build uses. For crates.io dependencies it
also stores checksums; why you commit it is explained on
The Crate Ecosystem and Supply-Chain Safety.
Organizing a program
A few habits keep a growing Rust program easy to find your way in:
-
Put logic in a library crate and keep
main.rsto reading arguments, calling the library and printing results. The library can then be tested without starting the program. -
Make a module per subject (
tariff,telegram,report), not per kind of thing (functions,structs). -
Start private and make items
pubonly when another module needs them. Everypubitem in a library is a promise to its users that is hard to take back; Cargo's versioning rules treat removing one as a breaking change (see semantic versioning). -
Keep functions short and give them a signature that says what they
need: a borrowed
&stror&[u32]when they only read, an owned value when they keep it. The next pages explain why.