You've been inside a function since the first line you wrote.
fn main() is one, and every println!(...) is a call (the ! marks it as a macro).
So instead of explaining what a function is, this chapter covers the parts of fn that Rust does its own way: explicit parameter and return types, blocks as expressions, and the trailing-semicolon rule that decides what gets returned.
fn add(a: i32, b: i32) -> i32 {
a + b
}
Reading left to right: fn says "this is a function", add is its name, the parentheses list the parameters with their types, and -> i32 declares the return type.
Every parameter type and the return type are spelled out explicitly.
Rust never guesses these for you.
To call a function, write its name with the arguments in parentheses:
let sum = add(2, 3); // sum: i32 = 5
The body of a function is a block, and a block is one or more statements followed by an optional final expression. The final expression (if there is one, and it has no trailing semicolon) becomes the value of the block, which becomes the return value of the function.
fn double(n: i32) -> i32 {
n * 2 // no semicolon: this is the return value
}
You can also use an explicit return, which is occasionally useful for early exits, but the no-semicolon form is more idiomatic for the final value:
fn double(n: i32) -> i32 {
return n * 2; // works too, but unusual at the end
}
That semicolon thing trips up newcomers.
The rule is short: a semicolon turns an expression into a statement (which has no value).
Forgetting one at the end of the function is the correct thing to do when you want the value to be returned.
Adding one accidentally turns the body into "do this, then return ()" and the compiler will complain that the types don't match.
The first exercise lets you feel that error first-hand.
The two exercises after it each pull at one more thread: returning a value built recursively, and modifying a parameter inside the body.
Start with the smallest function you can. Single-purpose functions are easier to test and easier to read.
Use parameter names that say what the value is, not what type it is: width: u32, not w: u32.
Prefer the least demanding parameter type that still lets you do the job.
"Least demanding" means: ask the caller for as little as possible.
If you only need to read a string, take &str, not String.
Three reasons this matters:
String would force the caller to hand over their value (or .clone() it).
Taking &str lets them keep it.&str parameter accepts string literals ("hi"), borrows of owned strings (&my_string coerces from &String to &str), and slices of larger buffers, all without conversion at the call site.&str is just a pointer and a length; passing one costs nothing.
A String parameter would mean moving (or cloning) a heap buffer on every call.The same idea extends to other types: take &[T] instead of &Vec<T>, &Path instead of &PathBuf, and so on.
We'll come back to this pattern in the vectors and ownership chapters.
This function takes an i32, declares an i32 return type, multiplies by two.
Yet, the compiler refuses to compile.
Run the tests, read the error, and fix it.
The lesson hiding behind that error is the difference between an expression (which has a value) and a statement (which doesn't). One character decides which a line is, and that character decides what your function returns.
/// Doubles `n`.
///
/// The exercise version is missing its return value: `n * 2;` with a
/// trailing semicolon is a statement, so the function returns `()`
/// instead of `i32`. Dropping the semicolon makes `n * 2` the final
/// expression, which is what gets returned.
fn double(n: i32) -> i32 {
n * 2
}
#[test]
fn test_double() {
assert_eq!(double(0), 0);
assert_eq!(double(3), 6);
assert_eq!(double(-7), -14);
}
This exercise is about recursion: a function that calls itself. Each call returns a value built up from the answer to a smaller version of the same problem.
Write sum_to(n) so it returns 1 + 2 + ... + n, with sum_to(0) == 0.
The base case and recursive case look like this:
sum_to(0) = 0 // base case
sum_to(n) = n + sum_to(n - 1) // for n > 0
That's the whole idea of recursion: a function's answer is defined in terms of its own answer to a smaller version of the same problem. Once the base case is reached, every pending call finishes its addition and the final total bubbles back up.
if with a base case (n == 0) and a recursive case that calls sum_to(n - 1).n + sum_to(n - 1).
No mut, no let, no return.
/// Returns the sum `1 + 2 + ... + n`, computed recursively.
/// By convention, `sum_to(0)` is `0`.
///
/// Each call returns its number plus the sum of everything below it;
/// `n == 0` is the base case that returns `0` and stops the recursion.
fn sum_to(n: u32) -> u32 {
if n == 0 { 0 } else { n + sum_to(n - 1) }
}
#[test]
fn test_sum_to() {
assert_eq!(sum_to(0), 0);
assert_eq!(sum_to(1), 1);
assert_eq!(sum_to(3), 6); // 1 + 2 + 3
assert_eq!(sum_to(10), 55);
assert_eq!(sum_to(100), 5_050);
}
Write cap_at(value, max) so it returns value if it's at or below max, and max otherwise.
Both arguments are i32.
The logic is one if away.
Write the function the most natural way you can think of.
The most natural way doesn't compile, because function parameters are immutable bindings by default (like let), so reassigning value is rejected.
The fix is one keyword in one place.
Read the error, it points right at it.
Once it compiles, look at the second test.
The caller's variable is untouched even though the function reassigned its parameter.
That's because i32 is Copy, so the function received its own copy to mutate.
The moves chapter showed the other half of this: a non-Copy type like String gets moved in instead of copied.
The next chapter, borrowing, shows how to lend a value to a function without giving it up at all.
value.
Function parameters are immutable bindings by default, just like let.mut to the parameter binding (not the type): fn cap_at(mut value: i32, max: i32) -> i32.
/// Returns `value` if it is at or below `max`, otherwise `max`.
///
/// The catch is the keyword `mut`: to reassign the parameter inside the
/// function it has to be declared `mut value`. Because `i32` is `Copy`,
/// the function mutates its own copy, so the caller's variable is left
/// untouched (see `caller_value_is_unchanged`).
fn cap_at(mut value: i32, max: i32) -> i32 {
if value > max {
value = max;
}
value
}
#[test]
fn test_cap_at() {
assert_eq!(cap_at(5, 10), 5);
assert_eq!(cap_at(10, 10), 10);
assert_eq!(cap_at(11, 10), 10);
assert_eq!(cap_at(-3, 10), -3);
assert_eq!(cap_at(1_000, 0), 0);
}
// The caller's variable is not affected by the function modifying
// its parameter. `i32` is `Copy`, so the function got its own copy.
#[test]
fn caller_value_is_unchanged() {
let original = 42;
let _capped = cap_at(original, 10);
assert_eq!(original, 42);
}
Three small exercises, three new ideas:
stray_semicolon drove home the difference between an expression and a statement: one trailing ; is the line between "return this value" and "return ()".sum_to built a value with recursion, where each call's answer feeds into the caller's answer.cap_at showed that function parameters are immutable bindings by default, and that adding mut to the parameter name affects only the function's own copy.
The caller's variable is untouched because i32 is Copy.The thread running through all of this: the body of a function is a block whose final expression (no trailing semicolon) is the return value. Everything else in the chapter is a consequence of that one rule.