Rust · Beginner
Functions in Rust
In short: A Rust function is declared with fn name(param: Type) -> ReturnType { body }. Parameter types are mandatory, the value of the last expression in the body (written without a semicolon) is returned, and return exits early.
What a function is in Rust
A function packages a computation behind a name so it can be called from several places and understood on its own. In Rust the declaration carries everything a caller needs to know: fn sheets_needed(pages: u32, per_sheet: u32) -> u32. Every parameter has a type, written after a colon, and the return type follows an arrow. The compiler does not infer parameter types from the calls, and that is deliberate: the signature is a contract that can be read, checked and relied on without looking at the body or at any caller. A function that returns nothing omits the arrow; its return type is then the empty tuple (), which is what println! evaluates to as well.
The body is a block, and a block's value is its final expression. So fn double(x: i32) -> i32 { x * 2 } returns x * 2 because that expression has no semicolon. Adding a semicolon would turn it into a statement and the block's value into (), which the compiler rejects as a type mismatch. This is the single rule to internalise: statements end with a semicolon and produce nothing; the last expression without one is the result. The return keyword still exists for leaving early from inside a loop or a condition, and return value; at the end of a body is legal but unidiomatic.
Because if, match and blocks are expressions too, a body often reads as one expression: if pages % per_sheet == 0 { full } else { full + 1 } is a complete return value with no temporary variable. The control flow lesson introduced this; functions are where it pays off.
Calls must match the signature exactly. Passing a float where an i32 is expected is an error, not a conversion. There are no default arguments and no overloading: one name, one parameter list. To return several values, return a tuple and destructure it at the call site: let (hours, minutes) = split_minutes(135);. Functions can be declared anywhere in the file, before or after main, and the order does not matter.
A detail that trips up newcomers is text parameters. A function that only reads a string should take &str, the borrowed string slice. Callers can pass a literal directly, or &name when they hold a String; Rust converts &String to &str automatically. Taking String instead would force every caller to hand over ownership, which the ownership lesson explains. format! is the sibling of println! that builds a String instead of printing, and it is how a function returns text it has composed.
Syntax
fn name(param: Type, other: Type) -> ReturnType {
let local = param + other; // a statement: ends with ;
local * 2 // the last expression is the return value
}
fn describe(label: &str) { // no arrow: returns ()
println!("label: {}", label);
}
fn split_minutes(total: u32) -> (u32, u32) {
(total / 60, total % 60) // two values returned as a tuple
}
fn discount(total: u32) -> u32 {
if total < 50 {
return 0; // early exit
}
total / 10
}Names use snake_case. Every parameter needs a type; the return type is written after -> and is () when omitted.
Defining and calling functions
One function returns a value; the other returns nothing and prints instead.
fn pallet_weight(crates: u32, kg_each: f64) -> f64 {
crates as f64 * kg_each
}
fn print_label(customer: &str, kg: f64) {
println!("{}: {:.1} kg", customer, kg);
}
fn main() {
let first = pallet_weight(8, 2.25);
print_label("Millbrook Print", first);
print_label("Quay Cafe", pallet_weight(3, 4.0));
}Output
Millbrook Print: 18.0 kg Quay Cafe: 12.0 kg
pallet_weight takes two typed parameters and returns an f64; its body is a single expression, so there is no semicolon and no return. print_label has no arrow, returns (), and exists for its printed side effect. The second call to print_label passes the result of pallet_weight directly as an argument, which works because the value has exactly the type the parameter expects. Both functions are defined above main, but they could equally be placed after it.
Early return and returning a pair
Three cent amounts arrive on input; each is passed through discount_percent, and split_minutes returns two values at once.
use std::io::BufRead;
fn discount_percent(total_cents: u32) -> u32 {
if total_cents < 5000 {
return 0;
}
if total_cents < 20000 {
return 5;
}
10
}
fn split_minutes(total: u32) -> (u32, u32) {
(total / 60, total % 60)
}
fn main() {
for line in std::io::stdin().lock().lines() {
let cents: u32 = line.unwrap().trim().parse().unwrap();
println!("{} cents -> {}% off", cents, discount_percent(cents));
}
let (hours, minutes) = split_minutes(135);
println!("Print run: {} h {} min", hours, minutes);
}Input given to the program: 4200 ↵ 5000 ↵ 25000
Output
4200 cents -> 0% off 5000 cents -> 5% off 25000 cents -> 10% off Print run: 2 h 15 min
discount_percent uses return twice to leave as soon as a tier is decided, and its last line is the bare expression 10 for everything else. The early returns and the final expression all produce a u32, which the signature promises. split_minutes bundles hours and minutes into a tuple; the caller destructures it with let (hours, minutes) = ... so both values get names. The loop reads the input line by line, exactly as in the control flow lesson, and calls the function for each.
Expressions all the way down, and a helper that reads input
The input lines are 14 and 4. Notice that read_line, sheets_needed and the block assigned to note each end in an expression without a semicolon.
use std::io::BufRead;
fn read_line() -> String {
std::io::stdin().lock().lines().next().unwrap().unwrap()
}
fn sheets_needed(pages: u32, per_sheet: u32) -> u32 {
let full = pages / per_sheet;
if pages % per_sheet == 0 { full } else { full + 1 }
}
fn main() {
let pages: u32 = read_line().trim().parse().unwrap();
let per_sheet: u32 = read_line().trim().parse().unwrap();
let sheets = sheets_needed(pages, per_sheet);
let note = {
let spare = sheets * per_sheet - pages;
if spare == 0 { String::from("no blank pages") } else { format!("{} blank page(s)", spare) }
};
println!("{} sheets, {}", sheets, note);
}Input given to the program: 14 ↵ 4
Output
4 sheets, 2 blank page(s)
read_line wraps the noisy input idiom in a one-line function that returns a String; each call takes the next line of standard input. sheets_needed computes with a let statement and then returns an if expression. In main, note is assigned from a block: spare is local to it, and the if inside chooses between two String values, so both branches have the same type. format! builds text the way println! prints it. Fourteen pages at four per sheet need four sheets, with two pages left blank.
Common mistakes
Ending the return expression with a semicolon
Why it goes wrong:
fn double(x: i32) -> i32 { x * 2; }fails with E0308, mismatched types: the semicolon turnsx * 2into a statement, the block's value becomes(), and the signature promisedi32. The compiler's help text says exactly which semicolon to remove.Fix: Leave the final expression bare, or write
return x * 2;.Rust · fixfn double(x: i32) -> i32 { x * 2 }Leaving the type off a parameter
Why it goes wrong:
fn add(a, b)is a syntax error: Rust never infers parameter types, because the signature must stand on its own as documentation and as the thing the type checker trusts.Fix: Annotate every parameter:
fn add(a: i32, b: i32) -> i32.Passing a String to a function that takes &str
Why it goes wrong:
word_count(name)withname: Stringis a type mismatch (E0308). AStringis not a&str, though a reference to one converts automatically.Fix: Pass a reference:
word_count(&name). The compiler's help text suggests the borrow.Rust · fixfn word_count(text: &str) -> usize { text.split_whitespace().count() } let name = String::from("quay cafe"); println!("{}", word_count(&name));Calling a function with an argument of the wrong numeric type
Why it goes wrong:
pallet_weight(8.0, 2.25)fails when the first parameter isu32; Rust does not convert8.0to an integer or8to a float for you.Fix: Pass a value of the declared type, converting at the call site with
asif you must.
Statements and expressions
| Code | Kind | Value |
|---|---|---|
let x = 5; | statement | none |
x + 1 | expression | the sum |
x + 1; | expression statement | () |
{ let y = 2; y * x } | block expression | y * x |
if a { 1 } else { 2 } | expression | 1 or 2 |
println!("hi") | expression | () |
Where you use this
Most exercises on this site want the same three things: read input, compute, print. Putting the computation into a function with a clear signature such as fn boxes_needed(items: u32, per_box: u32) -> u32 separates the logic from the reading and printing, so you can test it in your head with a couple of values before you run anything, and reuse it when the next exercise asks for the same arithmetic. A small read_line() helper hides the stdin().lock().lines().next().unwrap().unwrap() chain, and a function that returns a tuple, such as hours and minutes, is how you hand back two results without inventing a struct too early.
fn read_line() -> String {
std::io::stdin().lock().lines().next().unwrap().unwrap()
}
fn boxes_needed(items: u32, per_box: u32) -> u32 {
(items + per_box - 1) / per_box
}Key points
fn name(param: Type) -> Ret { }declares a function; parameter types are never inferred.- The last expression of the body, without a semicolon, is the return value; a semicolon makes it a statement.
return value;exits early; omitting the arrow means the function returns().- Arguments must match the parameter types exactly; there are no default arguments and no overloading.
- Return several values as a tuple and destructure them at the call.
- Take
&strfor text you only read; callers pass&namefor aString. - Functions may be declared in any order in the file.
Try it yourself
The program reads a number of items and the capacity of one box. Write a function boxes_needed(items: u32, per_box: u32) -> u32 that returns how many boxes are required, rounding up, and print Boxes: followed by its result.
use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let items: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
let per_box: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
println!("Items: {}", items);
// define boxes_needed above main and print "Boxes: N" here
}
8 ↵ 3Items: 8
Boxes: 3use std::io::BufRead;
fn boxes_needed(items: u32, per_box: u32) -> u32 {
(items + per_box - 1) / per_box
}
fn main() {
let mut lines = std::io::stdin().lock().lines();
let items: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
let per_box: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
println!("Items: {}", items);
println!("Boxes: {}", boxes_needed(items, per_box));
}
Practise this
Related lessons
Frequently asked questions
Why does my Rust function return () instead of the value?
Almost always because the last line ends with a semicolon. A semicolon turns an expression into a statement, and a block whose last item is a statement has the value (). Remove the semicolon from the final expression, or write return value; explicitly. The compiler's error message for this case points at the semicolon to remove.
Can a Rust function have default parameter values or be overloaded?
No. Each function has exactly one signature and every argument must be supplied. The usual alternatives are a second function with a more specific name, a parameter of type Option<T> where None means use the default, or a struct that carries the settings. Generic functions can accept several types, but that is a different mechanism from overloading.
Should a Rust function take String or &str?
Take &str whenever the function only reads the text. A &str parameter accepts string literals, slices, and references to a String, so it is the most flexible choice for callers. Take String only when the function needs to keep or modify the text, for example to store it in a struct; the ownership lesson explains why that difference exists.
How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.