What do you call a callback that rarely gets called?
Barely functioning.
If you’ve ever gone through customs, you know the importance of declaring what you’re bringing in. Rust takes that idea a step further: you declare both what goes in and what comes out of a function: its parameters and return type.
A function body is a block expression. Its final expression provides the return
value when it has no trailing semicolon. That means you can skip the return
keyword and just leave off the semicolon on the last expression, which looks
pretty nice.
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. Parameter types must be explicit.
Rust does not infer the return type from the function body.
Omitting the return type means () -- a.k.a. the unit type.
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: zero or more statements followed by an optional final expression. When that final expression has no trailing semicolon, its value becomes the value of the block. For a function body, that block value is also the function's return value.
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). Leave the semicolon off the
final expression when you want to return its value. Adding one accidentally
turns the body into "do this, then return ()" and the compiler will complain
that the types don't match. In the first exercise, you'll make that error
disappear by changing a single character.
Start with the smallest function you can. Single-purpose functions are easier to test and easier to read.
Use descriptive parameter names: width: u32, not w: u32.
Prefer the least demanding parameter type that still lets you do the job. If
you only need to read a string, take &str, not String. Taking String
would force the caller to hand over the value or clone it, while &str lets
them keep it. A &str parameter also accepts string literals, borrowed
String values, and slices of larger text buffers without conversion at the
call site. Borrowing does not clone the string, and moving a String would
transfer ownership without moving its heap allocation.
The same idea extends to other types, such as &[T] instead of &Vec<T> and
&Path instead of &PathBuf.
This function takes an i32, promises to return an i32, and multiplies the
input by two. Still, the compiler refuses to compile it.
Run the tests and read the error before you change anything.
The error points to the difference between an expression, which has a value, and a statement, which doesn't. One character decides which one the final line is, and therefore what the 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.
A recursive call uses ordinary function-call syntax, even inside the function being defined. It needs a base case that returns without calling itself again. Each other call should move closer to that case.
How does the answer for n relate to the answer for n - 1? Use that relation
to decide what each call should return.
if expression must produce the same type.
/// 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. Any implementation that passes the
tests is valid. You don't have to reassign a parameter to solve this.
Then try this experiment: use the signature
fn cap_at(value: i32, max: i32) -> i32, assign max to value when it
exceeds the limit, and return value. Run the tests and read the compiler's
explanation for rejecting the assignment. Change the parameter binding so this
version compiles. The chapter hints can help if you're stuck.
Look at the second test. The caller's variable is untouched even when the
function reassigns its parameter. That's because i32 is Copy, so the
function received its own copy to mutate. You saw the other half with moves: a
non-Copy type such as String is moved in instead of copied. Borrowing lets a
function use a value without taking ownership.
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);
}
// Even if the function reassigns its parameter, the caller keeps its original
// value: `i32` is `Copy`.
#[test]
fn caller_value_is_unchanged() {
let original = 42;
let _capped = cap_at(original, 10);
assert_eq!(original, 42);
}
Here's what each exercise asked you to work out:
stray_semicolon, one trailing ; changed an expression into a statement,
turning "return this value" into "return ()".sum_to, you built a value with recursion, where each call's answer feeds
into the caller's answer.cap_at, you saw that function parameters are immutable bindings by
default. Adding mut to the parameter name lets the function change its own
copy. The caller's variable is untouched because i32 is Copy.The central rule is that a function body is a block whose final expression,
without a trailing semicolon, becomes the return value. Parameters are bindings
too, so they are immutable unless you add mut inside the function.
Extra practice to explore at your own pace. These chapters do not count toward course progress.