Functions in Rust
Functions are the building blocks of readable, maintainable code. In Rust, functions are declared using the fn keyword and follow a clear, consistent syntax.
Basic Function Syntax
Here's how you define a simple function in Rust:
fn greet() {
println!("Hello, world!");
}
To call this function, you simply write greet();
Functions with Parameters
Functions can take parameters, which must have type annotations:
fn greet_person(name: &str) {
println!("Hello, {}!", name);
}
Return Values
Functions can return values. The return type is declared after an arrow ->:
fn add(x: i32, y: i32) -> i32 {
x + y // Note: no semicolon here!
}
Multiple Parameters
You can have multiple parameters with different types:
fn calculate_rectangle_area(width: f64, height: f64) -> f64 {
width * height
}
Functions That Return Nothing
Every function returns something. A function written without an -> still has a return type — it is (), pronounced "unit", the type with exactly one value and no information in it. That matters more than it sounds, because the semicolon rule and the unit type are the same rule seen from two sides: a body ending in value returns that value, and a body ending in value; returns () instead. Trace the program below, including the order of the two printed groups, before running it.
Predict
One of these functions declares a return type and one does not. Predict every printed line, in order, before running.
fn triple(value: i32) -> i32 {
value * 3
}
fn log_it(value: i32) {
println!("logging {}", value);
}
fn main() {
let tripled = triple(5);
let logged = log_it(tripled);
println!("tripled = {:?}", tripled);
println!("logged = {:?}", logged);
}The output is logging 15, then tripled = 15, then logged = (). Two things to take from it. A call runs where it is written — let logged = log_it(tripled); executes log_it at that line, which is why its output appears above main's own printing. And a function declared without an -> does not return "nothing"; it returns (), the unit type, a real value with exactly one possible form. That is the same rule as the semicolon rule seen from the other side: a body ending in value returns the value, and a body ending in value; returns () — which is why adding a stray semicolon to the last line of an -> i32 function breaks it with expected i32, found ().
Practice Time!
Try writing a function that:
- Takes two numbers as parameters
- Returns their sum
- Prints the result
fn main() {
let result = add_numbers(5, 3);
println!("The sum is: {}", result);
}
fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
Key Points to Remember
- Functions start with
fn - Parameter types must be declared
- Return types are declared with
-> - The last expression in a function is implicitly returned
- Early returns use the
returnkeyword - Function names use snake_case by convention
Now it's your turn to experiment with functions in the playground below!
Try It Yourself
Reading about functions is not the same as designing signatures under your own name. This is a build task: a small program that reports its own pass/fail. Three functions are stubbed out — one returns a single number from a tail expression, one returns two values as a tuple, and one calls the other two and formats their results into a sentence. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each function until every check passes and it prints All checks passed.
The third one is the point of the exercise: functions compose, so summarize should call perimeter and min_max rather than repeat their arithmetic. It also needs one thing from the previous lesson — min_max takes i32 values while summarize is handed f64 ones, so an as cast has to bridge them.
Build
Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, decide what that function's signature is promising, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.
// TODO 1: return the perimeter of a rectangle: two widths plus two heights.
// Use a TAIL EXPRESSION - no 'return' keyword, no trailing semicolon.
// perimeter(3.0, 4.0) -> 14.0
fn perimeter(width: f64, height: f64) -> f64 {
let _ = width;
let _ = height;
0.0
}
// TODO 2: return BOTH the smaller and the larger of two numbers, as a tuple
// in that order. A function returns several values by returning a tuple.
// min_max(9, 4) -> (4, 9), min_max(2, 2) -> (2, 2)
fn min_max(a: i32, b: i32) -> (i32, i32) {
let _ = a;
let _ = b;
(0, 0)
}
// TODO 3: return a one-line summary built by CALLING the two functions above
// rather than recomputing anything. min_max wants i32 values, so cast the
// f64 sides with 'as' before handing them over.
// summarize(3.0, 4.0) -> "perimeter 14 spans 3 to 4"
// Build it with format! and a {} for each of the three values.
fn summarize(width: f64, height: f64) -> String {
let _ = width;
let _ = height;
String::new()
}
// --- Build checks: these must all pass. Do not edit below this line. ---
fn main() {
assert_eq!(perimeter(3.0, 4.0), 14.0, "perimeter(3, 4) is 3+3+4+4 = 14");
assert_eq!(perimeter(2.5, 2.5), 10.0, "a 2.5 square has perimeter 10");
assert_eq!(min_max(9, 4), (4, 9), "min_max must return the SMALLER value first");
assert_eq!(min_max(2, 2), (2, 2), "equal inputs give equal outputs");
assert_eq!(
summarize(3.0, 4.0),
"perimeter 14 spans 3 to 4",
"summarize should reuse perimeter and min_max rather than recompute"
);
println!("All checks passed.");
println!("{}", summarize(3.0, 4.0));
println!("{:?}", min_max(9, 4));
}Expected output: All checks passed.
perimeter 14 spans 3 to 4
(4, 9)
Once it passes, try two variations and predict each before running:
- Put a semicolon on
perimeter's last line. Change the body's final line towidth * 2.0 + height * 2.0;. Predict what the compiler says before running. It refuses to compile witherror[E0308]: mismatched types, reportingexpected f64, found ()and pointing at the function signature with the note "implicitly returns()as its body has no tail orreturnexpression" — plus a help line offering to remove the semicolon. That single character turns the returned value into a discarded statement. - Return the tuple the other way round. In
min_max, swap the branches so the larger value comes first. Predict which check fails before running. Themin_max(9, 4)check fails withleft: (9, 4), right: (4, 9), while themin_max(2, 2)check still passes because both entries are equal. Tuple positions carry meaning that the type system cannot check for you —(i32, i32)is satisfied either way, so the ordering promise lives in the name and the docs, not in the signature.
Key Takeaways
- Functions are declared with
fnand can return values using->syntax - Rust functions must declare parameter types explicitly
- The last expression in a function body is implicitly returned (no
returnneeded) - Functions can return tuples to return multiple values
- A function with no
->returns(), the unit type
Next Steps
Now that you can write functions, you're ready to learn about ownership and borrowing — the system that guarantees memory safety without a garbage collector. It's Rust's most distinctive feature, and understanding it makes every later concept click into place.
Next lesson
Ownership & Borrowing
Rust ownership explained — understand move semantics, borrowing rules, and how Rust guarantees memory safety without a garbage collector
25 min