Functions & Expressions

Rust · Lesson 6 of 31 6 min read Updated

A function is a named block of code that does one job. You have already used main, the function where every program starts. Real programs split their work into many small functions, each easy to read and test.

Rust functions look familiar if you know C, Java or JavaScript, but they hide one important idea: almost everything in Rust is an expression that produces a value, and a single semicolon can change what a function returns. This lesson explains both.

Defining and calling functions

A function starts with fn, then its name in snake_case, its parameters in brackets and a body in curly brackets. Every parameter must have a type; Rust never guesses parameter types.

Rust
fn main() {
    greet("Farhan", 21);
    greet("Meera", 19);
}

fn greet(name: &str, age: u32) {
    println!("Hello {name}, you are {age} years old.");
}
Output
Hello Farhan, you are 21 years old.
Hello Meera, you are 19 years old.

greet is defined after main, and that is fine: Rust does not care about the order of functions in a file. The values you pass in a call ("Farhan", 21) are called arguments; the names in the definition (name, age) are parameters. The arguments must match the parameter types exactly, so greet("Farhan", -21) does not compile, because u32 cannot be negative.

Rust has no default parameter values and no overloading (two functions with the same name). If you need a variant, give it a different name, such as greet_formal.

Returning a value

The return type comes after an arrow ->. The value of the last expression in the body, written without a semicolon, is returned automatically:

Rust
fn total_with_gst(price_paise: u64) -> u64 {
    let gst = price_paise * 18 / 100;
    price_paise + gst
}

fn main() {
    let total = total_with_gst(129_900);
    println!("Total: {total} paise");
    println!("That is Rs. {}.{:02}", total / 100, total % 100);
}
Output
Total: 153282 paise
That is Rs. 1532.82

The price is kept in paise (a whole number) so no rounding errors creep in. {:02} prints a number with at least two digits, padding with a zero, so 5 paise would show as 05.

Statements and expressions

Rust code is made of two kinds of pieces:

  • A statement performs an action and produces no value. let gst = 10; is a statement, so let a = (let b = 5); is an error.
  • An expression evaluates to a value. 5 + 3, a function call, if and a block { ... } are all expressions.

Adding a semicolon to the end of an expression turns it into a statement and throws its value away. A block's value is its final expression, which lets you compute something in a few steps without a separate function:

Rust
fn main() {
    let overs = 7;
    let balls = {
        let full_overs = overs * 6;
        full_overs + 3
    };
    println!("Balls bowled: {balls}");
}
Output
Balls bowled: 45

The inner block's last line full_overs + 3 has no semicolon, so it becomes the value of the whole block, and full_overs disappears at the closing bracket.

The semicolon mistake

Here is the most common error new Rust programmers see. The last line ends with a semicolon, so the function body has no final expression:

Rust
fn gst_amount(price: u32) -> u32 {
    price * 18 / 100;
}
Output
error[E0308]: mismatched types
 --> src/main.rs:1:30
  |
1 | fn gst_amount(price: u32) -> u32 {
  |    ----------                ^^^ expected `u32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
2 |     price * 18 / 100;
  |                     - help: remove this semicolon to return this value

The body now returns (), the unit type that means "nothing", but the signature promised a u32. The compiler even points at the semicolon to remove. Remember the rule: the last expression, without a semicolon, is the return value.

Early return and returning several values

return leaves a function immediately. Use it for early exits, such as rejecting bad input at the top. To return more than one value, return a tuple:

Rust
fn metro_fare(age: u32) -> u32 {
    if age < 5 {
        return 0; // small children travel free
    }
    let base = 40;
    if age >= 60 { base / 2 } else { base }
}

fn split_bill(amount: u32, people: u32) -> (u32, u32) {
    (amount / people, amount % people)
}

fn main() {
    println!("Fare at 3: {}", metro_fare(3));
    println!("Fare at 25: {}", metro_fare(25));
    println!("Fare at 67: {}", metro_fare(67));

    let (each, left_over) = split_bill(1000, 3);
    println!("Each friend pays {each}, {left_over} rupee left over");
}
Output
Fare at 3: 0
Fare at 25: 40
Fare at 67: 20
Each friend pays 333, 1 rupee left over

In metro_fare, the final if ... else ... is an expression, so its value (20 or 40) is the return value. Both branches must have the same type. The caller unpacks the tuple from split_bill with destructuring, as you saw in Scalar & Compound Types.

Documenting functions

Ordinary comments start with //. Doc comments start with /// and describe the item below them. Tools such as cargo doc and your editor show them, and any code examples inside them can be run as tests (see Testing: Unit, Integration & Doc Tests).

Rust
/// Returns the number of whole pages needed to print `lines` lines,
/// when one page holds `per_page` lines.
fn pages_needed(lines: u32, per_page: u32) -> u32 {
    lines.div_ceil(per_page)
}

div_ceil divides and rounds up, so pages_needed(101, 50) is 3.

Common mistakes

  • A semicolon after the final expression. The function then returns () and you get "mismatched types". Remove the semicolon.
  • Leaving out parameter types. fn area(w, h) is an error; write fn area(w: u32, h: u32) -> u32.
  • if branches with different types. if x { 1 } else { "one" } cannot be one value.
  • Expecting default arguments. Rust has none; use separate functions or pass an Option.

Key points

  • Functions are declared with fn, parameters always have types, and the return type follows ->.
  • The last expression without a semicolon is the function's return value; return is for early exits.
  • Statements perform actions; expressions produce values. Blocks and if are expressions.
  • A semicolon after the final expression makes the function return (), causing a type error.
  • Return several values with a tuple, and document functions with /// doc comments.

Exercise

Write a function electricity_bill(units: u32) -> u32 for a town in Kochi: the first 100 units cost 4 rupees each and every unit above 100 costs 6 rupees. Use an early return for bills of 100 units or fewer. Call it from main for 80 units and for 250 units and print both bills.

Show solution
Rust
fn electricity_bill(units: u32) -> u32 {
    if units <= 100 {
        return units * 4;
    }
    let extra_units = units - 100;
    100 * 4 + extra_units * 6
}

fn main() {
    println!("80 units: Rs. {}", electricity_bill(80));
    println!("250 units: Rs. {}", electricity_bill(250));
}
Output
80 units: Rs. 320
250 units: Rs. 1300

For 250 units the first 100 cost 400 and the remaining 150 cost 900, giving 1300. The final line has no semicolon, so its value is returned.

Quick Quiz

1. What does a function return if its last line is a + b; (with a semicolon) and it has no return?

2. Which signature is valid Rust?

3. What is the value of x after let x = { let y = 4; y * 2 };?

4. How can a Rust function return two values?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call