Functions

Write functions with parameters and return values, and learn the one quirk — expressions versus statements — that trips up almost everyone coming from another language.

10 min read4 learning objectives

What You'll Learn

  • Define functions with parameters and return types
  • Understand the difference between statements and expressions
  • Return values without writing the word "return"
  • Follow Rust naming conventions that the compiler will nudge you toward anyway

You've already written one function — main, the entry point every Rust program needs. Let's go further: write your own, pass data into them, and get values back out.

Defining a function

Use the fn keyword, a name, parentheses for parameters, and curly braces for the body. Rust doesn't care where in the file a function is defined relative to where it's called — unlike some languages, there's no "must be declared before use" rule.

Notice greet is defined after it's used in main. That's perfectly fine — Rust reads the whole file before deciding whether anything is wrong.

A note on naming

Rust convention is snake_case for function and variable names — calculate_total, not calculateTotal or CalculateTotal. This isn't just a style guide suggestion: the compiler will actually print a warning if you stray from it. Rust has opinions, and honestly, that consistency makes other people's code easier to read on day one.

Parameters

Parameters need explicit types — always. This is one place Rust won't infer anything for you, and that's by design: a function's signature is a contract, and contracts should be unambiguous to anyone reading just the one line.

Return values

Here's the part that surprises people coming from other languages: in Rust, the last expression in a function is automatically what it returns — no return keyword required. You just... don't put a semicolon on it.

The -> i32 after the parentheses declares the return type. You can still use return explicitly — it's useful for returning early from inside an if or a loop — but for the final value of a function, leaving off the semicolon is the idiomatic Rust way.

The real key: statements vs. expressions

That semicolon trick isn't a quirk — it's a direct consequence of a deeper idea that runs through the entire language. Rust draws a hard line between two kinds of things:

  • Statements perform an action and return nothing. let x = 5; is a statement — it binds a value to a name, full stop.
  • Expressions evaluate to a value. 5 + 6 is an expression. So is a function call. So, importantly, is a block of code wrapped in { }.

That last one is the part that unlocks everything. A { } block evaluates to whatever its final, semicolon-free expression evaluates to — and you can use that block anywhere a value is expected:

Adding a semicolon turns an expression into a statement — and statements evaluate to nothing. This is exactly why a function body that ends with x * x; (semicolon included) would fail to compile if the function promised to return an i32: you've turned your return value into "nothing," and Rust will tell you so, clearly, at compile time.

Try it yourself: add a semicolon to the end of x * x in the square example above and try to run it. Read the compiler error carefully — it will tell you almost exactly what's wrong and how to fix it. Learning to enjoy reading Rust's error messages is a real career skill.

Putting it together

Once this clicks, you start writing more naturally — fewer temporary variables, fewer explicit returns, more "this block is just an expression that produces a value":

See how the entire if/else if chain is itself an expression, assigned directly to feeling? No temporary mutable variable, no repeated assignments in each branch. We'll dig much deeper into if and friends in the next lesson.

Quick exercise

Write a function fn is_even(n: i32) -> bool that returns whether a number is even, without using the word return anywhere. Then write a second function that takes a slice of numbers and returns how many of them are even, calling your first function from inside it.

You can now write and call your own functions, and — more importantly — you understand why Rust code looks the way it does. That single idea, "blocks are expressions," will keep paying off as we go.