Functions
So far almost everything has happened inside main. Real programs split their
work into functions: named pieces of code that take some inputs, do one job
and hand back a result. You have already called plenty of them, from
String::from to len(), and written a few small ones in passing.
This lesson covers functions properly: how to define them, how parameters and return values are typed, and the idea that makes Rust functions read the way they do, the difference between statements and expressions.
Defining and calling a function
A function starts with fn, then its name, a parameter list in parentheses and
a body in braces:
fn greet() {
println!("Welcome back!");
}
fn main() {
greet();
greet();
}Welcome back!
Welcome back!Call a function by writing its name followed by parentheses. Every call runs the whole body again.
Function names use snake_case: lowercase words joined by underscores, like
print_report or total_price. The compiler warns if you use another style.
The order of functions in a file doesn't matter. main can call a function
that is defined below it, and in real code main often comes first so a reader
sees the big picture before the details.
Parameters
Inputs are declared as parameters, each with a name and a type:
fn print_labeled(label: &str, value: f64) {
println!("{label}: {value:.1}");
}
fn main() {
print_labeled("Temperature", 21.456);
print_labeled("Humidity", 64.0);
}Temperature: 21.5
Humidity: 64.0Unlike let, parameters always need a type annotation. Type inference works
inside a function, but a function's signature is a contract with every caller,
so Rust makes you spell it out. In return, the compiler checks every call
against it: the right number of arguments, each of the right type.
The values you pass in a call are called arguments. They are matched to
the parameters by position, and each parameter behaves like a local variable
for the rest of the body. Parameters are immutable unless you write mut
before the name, just like variables:
fn countdown(mut n: u32) {
while n > 0 {
print!("{n} ");
n -= 1;
}
println!("liftoff!");
}
fn main() {
let start = 3;
countdown(start);
println!("start is still {start}");
}3 2 1 liftoff!
start is still 3countdown gets its own copy of the number, so changing n doesn't touch
start. That is true for simple values like integers. For values like
String, passing them to a function has consequences of its own, which the
ownership lesson explains.
Return values
A function that produces a value declares its type after an arrow, ->. The
value of the last expression in the body is what the function returns:
fn fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}
fn main() {
let boiling = fahrenheit(100.0);
println!("{boiling}°F");
println!("{}°F", fahrenheit(-40.0));
}212°F
-40°FNote what is missing: there is no return keyword and no semicolon after
celsius * 9.0 / 5.0 + 32.0. That line is the function's final expression, so
its value is the return value. This is the normal way to return in Rust.
A call is itself an expression, so you can use it anywhere a value fits: in a
let, as an argument to another function, or inside a format string.
To return more than one value, return a tuple, as in the previous lesson. A
function without -> returns the unit type ().
Statements and expressions
To see why the missing semicolon matters, you need two words:
- A statement performs an action and produces no value.
let x = 5;is a statement, and so is any expression followed by a semicolon. - An expression evaluates to a value.
5,x + 1,fahrenheit(20.0)and a block in braces are all expressions.
A semicolon turns an expression into a statement by throwing its value away.
That is why the last line of fahrenheit has none: it is the value the whole
function body evaluates to.
Blocks are expressions too. The value of a block is the value of its last expression, which lets you compute a value in a few steps without naming every intermediate result outside the block:
fn main() {
let width = 12;
let height = 5;
let diagonal = {
let squares = width * width + height * height;
(squares as f64).sqrt()
};
println!("diagonal: {diagonal}");
}diagonal: 13squares only exists inside the block. The block's value, 13.0, is what
diagonal is bound to. A function body is exactly this kind of block, which is
why its last expression becomes the return value.
Because let is a statement, it has no value, so you can't write
let a = let b = 1;. Assignment is different again: b = 1 is an expression,
but its value is (), so a = b = 1 doesn't set both variables the way it does
in some other languages.
Returning early with return
Sometimes a function knows its answer before reaching the end. return exits
immediately with a value:
fn shipping_cost(weight_kg: f64) -> f64 {
if weight_kg <= 0.0 {
return 0.0;
}
let base = 4.5;
base + weight_kg * 1.2
}
fn main() {
println!("{:.2}", shipping_cost(0.0));
println!("{:.2}", shipping_cost(2.5));
}0.00
7.50Use return for these early exits, like rejecting input up front, and let the
final expression carry the normal result. Writing return x; as the last line
also works, but most Rust code leaves it out.
Functions that never return
A few functions never hand control back to their caller: they end the program
or loop forever. Their return type is !, called the never type. You will
see it in the standard library, for example on std::process::exit, and it is
what panic! evaluates to. You rarely write -> ! yourself, but it explains
why panic!() can stand in for a value of any type, which comes up in the
error-handling lesson.
Documenting a function
A comment that starts with /// right above a function is a doc comment.
Editors show it when you hover over a call, and cargo doc turns it into HTML
documentation:
/// Returns the number of whole boxes needed to pack `items`,
/// with `per_box` items in each box.
fn boxes_needed(items: u32, per_box: u32) -> u32 {
items.div_ceil(per_box)
}
fn main() {
println!("{}", boxes_needed(25, 10));
}3Describe what the function does and what the parameters mean, not how the body works.
Functions in other modules
Everything in this lesson has been in one file. Once code is split into
modules, a function is private to its module unless it is marked pub:
mod units {
pub fn km_to_miles(km: f64) -> f64 {
km * 0.621371
}
}
fn main() {
println!("{:.1}", units::km_to_miles(42.195));
}26.2Without pub, the call from main would fail to compile. Library code, like
the functions a Rustfinity challenge asks for, is used from outside its module,
so it needs pub. Modules and visibility get more attention later; for now,
read pub as "callable from outside".
Common mistakes
A parameter without a type
fn double(n) -> i32 {
n * 2
}
fn main() {
println!("{}", double(4));
}Parameters are never inferred. Write n: i32.
Calling with the wrong arguments
fn area(width: u32, height: u32) -> u32 {
width * height
}
fn main() {
println!("{}", area(3));
}The compiler shows the signature and which argument is missing. Passing an
argument of the wrong type, like area(3, 4.5), fails the same way with
mismatched types. Rust has no default or optional parameters, so every call
passes every argument.
Forgetting the return type
A function without -> returns (), even if its last line is a value:
fn half(n: i32) {
n / 2
}
fn main() {
let x = half(10);
println!("{x:?}");
}The compiler suggests the fix: add -> i32 to the signature. The opposite
mistake, a return type with a semicolon after the last expression, is the one
from the previous lesson: remove the semicolon so the value is returned.
Using let as a value
fn main() {
let total = let subtotal = 40;
println!("{total}");
}let is a statement, so it has no value to assign. Declare subtotal on its
own line first, or use a block if you want both names scoped together.
Summary
- Define a function with
fn name(param: Type, ...) -> ReturnType { ... }. Names aresnake_case, and definition order doesn't matter. - Every parameter needs a type. Arguments are matched to parameters by position, and every call must pass all of them.
- A function returns the value of its last expression. Leave off the
semicolon; use
returnfor early exits. - Statements do something and have no value (
let, anything ending in;). Expressions produce a value, and blocks are expressions too. - Without
->, a function returns(). ///doc comments document a function;pubmakes it callable from other modules.
Practise what you read
Challenges that use the ideas from this lesson.