Functions

Rust functions look close to C's, with one distinctly Rust habit: the last expression in a function body — with no semicolon — is automatically its return value, no return keyword required.

Declaring a function

Rust src/main.rs
fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let sum = add(3, 4);
    println!("Sum: {}", sum);
}
Output
Sum: 7

Parameter types are required (Rust never infers them for you), and -> i32 declares the return type. Notice a + b has no semicolon and no return — that's what makes it the function's return value. Add a semicolon and the meaning changes completely:

Rust src/main.rs — this won't compile
fn add(a: i32, b: i32) -> i32 {
    a + b;
}
Compiler output
error[E0308]: mismatched types
  |
1 | fn add(a: i32, b: i32) -> i32 {
  |                            --- expected `i32` because of return type
2 |     a + b;
  |          ^ help: remove this semicolon to return this value

A semicolon turns a + b from an expression into a statement — a statement produces no value at all, so the function's body ends up with nothing to return, even though it declared -> i32. This trips up almost everyone coming from a semicolon-everywhere language at first.

You can still use return early

Rust src/main.rs
fn classify(n: i32) -> &'static str {
    if n < 0 {
        return "negative";
    }
    if n == 0 {
        return "zero";
    }
    "positive"
}

fn main() {
    println!("{}", classify(-5));
    println!("{}", classify(0));
    println!("{}", classify(5));
}
Output
negative
zero
positive

Explicit return works exactly as it does elsewhere, and is normal for exiting early. The convention in Rust is to use the no-semicolon final expression for the "normal" return value, and return only for early exits — mixing styles for the same purpose in one function tends to read as inconsistent.

Passing ownership vs. borrowing into a function

This is where the Ownership lesson comes back directly: passing a String by value moves it into the function, while passing &String only borrows it.

Rust src/main.rs
fn shout(text: &String) -> String {
    text.to_uppercase()
}

fn main() {
    let message = String::from("hello");
    let shouted = shout(&message);
    println!("{} -> {}", message, shouted);
}
Output
hello -> HELLO
Note: as a default habit, prefer borrowing (&String, &str, &Vec<T>) for parameters that a function only needs to read, and only take ownership when the function genuinely needs to consume or store the value. This keeps the caller free to keep using their original variable afterward, same as shout leaves message usable above.