Lesson 7 of 94
Functions
The key idea is that almost everything in Rust is an expression that produces a value, including blocks. That changes how you return values.
| Concept | Rust |
|---|---|
| Function with a return value | fn add(a: i32, b: i32) -> i32 { a + b } |
| Function with no return value | fn log(msg: &str) |
| Overloading | Not supported: use different names |
| Default parameter values | Not supported |
| Failure | panic!(...) for bugs, Result for expected errors |
1. Defining and calling a function
fn greet() {
println!("Welcome to the store!");
}
fn main() {
greet();
greet();
}Welcome to the store!
Welcome to the store!Functions use snake_case names. They can be defined in any order: main can call a function written below it.
2. Parameters must have types
Every parameter needs a type annotation. Rust never infers parameter types, because a function's signature is its contract with its callers.
fn print_order(order_id: u32, quantity: i32) {
println!("Order #{order_id}: {quantity} item(s)");
}
fn main() {
print_order(1001, 3);
print_order(1002, 1);
}Order #1001: 3 item(s)
Order #1002: 1 item(s)Leaving out a type is an error:
fn double(x) {
println!("{}", x * 2);
}
fn main() {
double(4);
}error: expected one of `:`, `@`, or `|`, found `)`
--> src/main.rs:1:12
|
1 | fn double(x) {
| ^ expected one of `:`, `@`, or `|`
|
help: if this is a parameter name, give it a type
|
1 | fn double(x: TypeName) {
| ++++++++++
help: if this is a type, explicitly ignore the parameter name
|
1 | fn double(_: x) {
| ++3. Returning a value
Write the return type after ->. The last expression of the body, written without a semicolon, is the return value.
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let sum = add(19, 23);
println!("19 + 23 = {sum}");
}19 + 23 = 424. The semicolon mistake
Adding a semicolon turns an expression into a statement, which produces no value. This is the most common error for newcomers, and the compiler explains it well.
fn add(a: i32, b: i32) -> i32 {
a + b;
}
fn main() {
println!("{}", add(1, 2));
}error[E0308]: mismatched types
--> src/main.rs:1:27
|
1 | fn add(a: i32, b: i32) -> i32 {
| --- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | a + b;
| - help: remove this semicolon to return this value5. return for early exit
Idiomatic Rust uses return for early exits and lets the final expression be the normal result.
fn shipping_cost(order_total: f64) -> f64 {
if order_total >= 50.0 {
return 0.0;
}
4.99
}
fn main() {
println!("{}", shipping_cost(75.0));
println!("{}", shipping_cost(20.0));
}0
4.996. Blocks are expressions
A block { ... } evaluates to its last expression. You can use a block anywhere a value is expected, which lets you compute a value with temporary variables that do not leak out.
fn main() {
let area = {
let width = 12;
let height = 5;
width * height
};
println!("Area: {area}");
}Area: 607. if is an expression too
Because if produces a value, you can return it directly. This replaces the ternary operator ? : found in other languages.
fn describe(temperature: i32) -> &'static str {
if temperature < 0 {
"freezing"
} else if temperature < 20 {
"cold"
} else {
"warm"
}
}
fn main() {
println!("{}", describe(-5));
println!("{}", describe(12));
println!("{}", describe(28));
}freezing
cold
warmThe return type &'static str means "text that lives for the whole program", such as a string literal. Read it as "a string literal" for now. Lifetime Elision & 'static explains it fully.
8. Functions that return nothing return ()
A function without -> returns the unit type (), an empty value. It is a real value you can store and print.
fn log_event(event: &str) {
println!("[event] {event}");
}
fn main() {
let result = log_event("user_signed_in");
println!("log_event returned {:?}", result);
}[event] user_signed_in
log_event returned ()9. Parameters are copies for simple types
Numbers, booleans, and characters are copied into the function. Changing the parameter never changes the caller's variable. To change a parameter inside the function, mark it mut.
fn countdown_from(mut n: i32) {
n -= 1;
println!("inside: {n}");
}
fn main() {
let start = 5;
countdown_from(start);
println!("caller still has: {start}");
}inside: 4
caller still has: 5For non-simple types such as String, passing a value moves it instead of copying. That is the subject of the whole Ownership section.
10. No overloading
Rust does not allow two functions with the same name in the same scope.
fn area(side: f64) -> f64 {
side * side
}
fn area(width: f64, height: f64) -> f64 {
width * height
}
fn main() {
println!("{}", area(2.0));
}error[E0428]: the name `area` is defined multiple times
--> src/main.rs:5:1
|
1 | fn area(side: f64) -> f64 {
| ------------------------- previous definition of the value `area` here
...
5 | fn area(width: f64, height: f64) -> f64 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `area` redefined here
|
= note: `area` must be defined only once in the value namespace of this moduleUse descriptive names instead. Later you will also see traits and generics, which cover most real uses of overloading.
fn square_area(side: f64) -> f64 {
side * side
}
fn rectangle_area(width: f64, height: f64) -> f64 {
width * height
}
fn main() {
println!("{}", square_area(2.0));
println!("{}", rectangle_area(2.0, 3.5));
}4
711. No default parameter values
Rust has no default arguments. The usual pattern is a short wrapper function that fills in the default. Later you will see the builder pattern and the Default trait for structs with many options.
fn connect_with_timeout(host: &str, timeout_secs: u32) {
println!("connecting to {host} (timeout {timeout_secs}s)");
}
fn connect(host: &str) {
connect_with_timeout(host, 30);
}
fn main() {
connect("db.internal");
connect_with_timeout("cache.internal", 5);
}connecting to db.internal (timeout 30s)
connecting to cache.internal (timeout 5s)12. Functions calling functions
Small functions that each do one thing combine well.
fn celsius_to_fahrenheit(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
fn fahrenheit_to_celsius(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
fn round_to_one_decimal(x: f64) -> f64 {
(x * 10.0).round() / 10.0
}
fn main() {
let boiling = celsius_to_fahrenheit(100.0);
let body = fahrenheit_to_celsius(98.6);
println!("100°C = {}°F", boiling);
println!("98.6°F = {}°C", round_to_one_decimal(body));
}100°C = 212°F
98.6°F = 37°C13. Returning several values with a tuple
A function can return several values at once in a tuple, written (a, b). There is no need to create a class just to hold two results. Tuples & Arrays covers tuples in detail.
fn divide_with_remainder(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
fn main() {
let (hours, minutes) = divide_with_remainder(135, 60);
println!("135 minutes = {hours}h {minutes}m");
}135 minutes = 2h 15m14. Recursion
fn factorial(n: u64) -> u64 {
if n == 0 { 1 } else { n * factorial(n - 1) }
}
fn main() {
println!("5! = {}", factorial(5));
println!("20! = {}", factorial(20));
}5! = 120
20! = 243290200817664000020! is the largest factorial that fits in a u64. 21! would overflow and panic in a debug build. Rust does not guarantee tail-call optimization, so very deep recursion can overflow the stack. Prefer loops for deep iteration.
15. Functions inside functions
You can define a helper function inside another function. It is only visible there, which keeps it out of the rest of the file.
fn format_duration(total_seconds: u32) -> String {
fn pad(n: u32) -> String {
format!("{n:02}")
}
let hours = total_seconds / 3600;
let minutes = total_seconds % 3600 / 60;
let seconds = total_seconds % 60;
format!("{}:{}:{}", pad(hours), pad(minutes), pad(seconds))
}
fn main() {
println!("{}", format_duration(3725));
println!("{}", format_duration(59));
}01:02:05
00:00:59The return type String is owned, growable text. It is introduced in A First Look at String, Vec & HashMap.
16. panic! for bugs
panic! stops the program with an error message. Use it for situations that should be impossible if the code is correct, such as a failed assert. Expected failures such as bad user input use Result, covered in Result & the ? Operator.
fn percentage(part: u32, whole: u32) -> u32 {
if whole == 0 {
panic!("percentage() called with whole = 0");
}
part * 100 / whole
}
fn main() {
println!("{}%", percentage(1, 4));
println!("{}%", percentage(1, 0));
}25%
thread 'main' panicked at src/main.rs:3:9:
percentage() called with whole = 0The badge panics at runtime marks examples that deliberately end in a panic. The output shows what was printed before the panic, then the panic message with the file, line, and column.
17. Runtime integer overflow panics in debug builds
The previous lesson showed that the compiler rejects overflow it can see. When the values only arrive at runtime, a debug build panics instead:
fn add_stock(current: u8, received: u8) -> u8 {
current + received
}
fn main() {
println!("{}", add_stock(100, 50));
println!("{}", add_stock(250, 10));
}150
thread 'main' panicked at src/main.rs:2:5:
attempt to add with overflowIn a release build (cargo run --release) the second call would silently return 4. That is why production code that could overflow should call checked_add, saturating_add, or wrapping_add explicitly.
18. Functions that never return: !
A function whose return type is ! (the "never" type) never returns normally: it always panics, loops forever, or exits the process. The compiler uses this knowledge, for example so you can call it where any type of value is expected.
fn fail(message: &str) -> ! {
panic!("fatal: {message}");
}
fn port_from_config(configured: i32) -> u16 {
if configured > 0 && configured <= 65535 {
configured as u16
} else {
fail("port out of range")
}
}
fn main() {
println!("port {}", port_from_config(8080));
println!("port {}", port_from_config(70000));
}port 8080
thread 'main' panicked at src/main.rs:2:5:
fatal: port out of range19. const fn: functions the compiler can run
A const fn can be evaluated at compile time, so it can initialize a const. The result is computed during compilation and costs nothing at runtime.
const fn kilobytes(n: u64) -> u64 {
n * 1024
}
const fn megabytes(n: u64) -> u64 {
kilobytes(n) * 1024
}
const MAX_UPLOAD_BYTES: u64 = megabytes(25);
fn main() {
println!("Max upload: {MAX_UPLOAD_BYTES} bytes");
println!("At runtime too: {}", kilobytes(4));
}Max upload: 26214400 bytes
At runtime too: 409620. Comments and documentation comments
// is a line comment and /* */ is a block comment. /// is a documentation comment. It is written in Markdown, and cargo doc turns it into HTML.
/// Returns the price after applying a percentage discount.
///
/// `percent` must be between 0 and 100.
fn discounted(price: f64, percent: f64) -> f64 {
// Convert the percentage to a multiplier, e.g. 15.0 -> 0.85
let multiplier = 1.0 - percent / 100.0;
/* A block comment works too. */
price * multiplier
}
fn main() {
println!("{:.2}", discounted(80.0, 15.0));
}68.0021. Realistic example: a loan payment calculator
This implements the standard amortization formula as a pure function.
/// Monthly payment for a fixed-rate loan.
fn monthly_payment(principal: f64, annual_rate_percent: f64, years: u32) -> f64 {
let monthly_rate = annual_rate_percent / 100.0 / 12.0;
let months = (years * 12) as i32;
if monthly_rate == 0.0 {
return principal / months as f64;
}
let factor = (1.0 + monthly_rate).powi(months);
principal * monthly_rate * factor / (factor - 1.0)
}
fn main() {
let payment = monthly_payment(250_000.0, 4.5, 30);
let total_paid = payment * 360.0;
println!("Monthly payment: {payment:.2}");
println!("Total paid: {total_paid:.2}");
println!("Total interest: {:.2}", total_paid - 250_000.0);
println!("Zero interest: {:.2}", monthly_payment(12_000.0, 0.0, 1));
}Monthly payment: 1266.71
Total paid: 456016.78
Total interest: 206016.78
Zero interest: 1000.0022. Realistic example: validating a password
Small bool-returning functions make rules easy to read and test.
const MIN_LENGTH: usize = 12;
fn is_long_enough(password: &str) -> bool {
password.len() >= MIN_LENGTH
}
fn has_digit(password: &str) -> bool {
password.chars().any(|c| c.is_ascii_digit())
}
fn has_uppercase(password: &str) -> bool {
password.chars().any(|c| c.is_uppercase())
}
fn is_strong(password: &str) -> bool {
is_long_enough(password) && has_digit(password) && has_uppercase(password)
}
fn main() {
println!("{}", is_strong("hunter2"));
println!("{}", is_strong("correcthorsebattery"));
println!("{}", is_strong("Correct-Horse-Battery-9"));
}false
false
truepassword.chars().any(|c| c.is_ascii_digit()) checks whether any character is a digit. It uses an iterator and a closure (Rust's lambda syntax, |c| ...). Both get full lessons later: Closures and Iterators.
23. Realistic example: tiered shipping rules
Early returns handle special cases first, and the last expression handles the general case.
fn shipping_cents(weight_grams: u32, is_express: bool, is_member: bool) -> u32 {
if weight_grams == 0 {
return 0;
}
if is_member && !is_express {
return 0;
}
let base = if weight_grams <= 500 {
499
} else if weight_grams <= 2_000 {
899
} else {
899 + (weight_grams - 2_000) / 1_000 * 250
};
if is_express { base * 2 } else { base }
}
fn main() {
println!("{}", shipping_cents(300, false, false));
println!("{}", shipping_cents(1_500, true, false));
println!("{}", shipping_cents(5_500, false, false));
println!("{}", shipping_cents(5_500, false, true));
println!("{}", shipping_cents(5_500, true, true));
}499
1798
1649
0
329824. Realistic example: a leap year and days-in-month helper
fn is_leap_year(year: u32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn days_in_month(year: u32, month: u32) -> u32 {
if month == 2 {
if is_leap_year(year) { 29 } else { 28 }
} else if month == 4 || month == 6 || month == 9 || month == 11 {
30
} else {
31
}
}
fn main() {
println!("Feb 2024: {}", days_in_month(2024, 2));
println!("Feb 2100: {}", days_in_month(2100, 2));
println!("Feb 2000: {}", days_in_month(2000, 2));
println!("Sep 2026: {}", days_in_month(2026, 9));
}Feb 2024: 29
Feb 2100: 28
Feb 2000: 29
Sep 2026: 30Summary
fn name(param: Type) -> ReturnType { ... }. Parameter types are always required.- The last expression without a semicolon is the return value.
returnis for early exits. - Blocks and
ifare expressions that produce values. - A function with no
->returns(), the unit type. - No overloading and no default arguments: use distinct names and small wrapper functions.
panic!is for bugs.-> !marks a function that never returns.const fncan run at compile time.///writes documentation.
Next: Control Flow.
