4. Functions
Functions in Rust hold few surprises. Closures hold a few more, because a closure has to say what it does with the variables it captures, and that turns out to be the same question ownership answers everywhere else. This chapter takes both, and leaves modules and testing to Chapter 10.
4.1 Declarations
#![allow(unused)]
fn main() {
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
}
Parameter types and the return type are required; nothing about a signature is inferred. Inside the body, inference does the work.
Functions may be declared in any order and a function may call one declared later in the file. There are no forward declarations and no header files.
The value of the function is the value of its final expression, written without
a semicolon. return exists for leaving early:
#![allow(unused)]
fn main() {
fn first_word(text: &str) -> &str {
for (i, c) in text.char_indices() {
if c == ' ' {
return &text[..i];
}
}
text
}
}
A function with no arrow returns (), and return; in such a function is
legal. A function that never returns at all, such as one that always panics or
loops forever, can be declared -> !.
A parameter may be declared mut, as gcd’s two are. The function then has a
modifiable copy of the argument, and the caller sees nothing. mut on a
parameter is part of the body, not part of the interface; callers never write it
and it does not appear in documentation.
4.2 Arguments
Arguments are passed by value. For a number or a bool or a char, “by value”
means a copy, and the cost is a register.
For anything that owns memory, passing by value hands over the ownership. The
caller no longer has the value. That is often what you want, and when it is not,
you pass a reference with & and the caller keeps it:
#![allow(unused)]
fn main() {
fn count_vowels(text: &str) -> usize {
text.chars().filter(|c| "aeiou".contains(*c)).count()
}
}
Taking &str rather than String means this function works on a String, on a
string literal, and on a slice of either, and it borrows rather than consumes.
That is the default shape for a function that only reads its argument, and
Chapter 5 explains what the ampersand costs and guarantees.
4.3 Recursion
Recursion works as you would expect, and Rust does not promise tail-call elimination, so a recursive function deep enough to matter will exhaust the stack. For the depths that come up in ordinary work it is fine:
#![allow(unused)]
fn main() {
fn fib(n: u32) -> u64 {
if n < 2 {
u64::from(n)
} else {
fib(n - 1) + fib(n - 2)
}
}
}
This is the slow definition, doing exponential work to recompute the same values. The loop version is three lines and instantaneous. Prefer iteration where it is natural; keep recursion for data that is recursive, such as the trees in Chapter 6.
4.4 Closures
A closure is a function written inline that can capture variables from around it:
#![allow(unused)]
fn main() {
let factor = 3;
let scale = |x: i32| x * factor;
println!("{} {}", scale(5), scale(7));
}
The parameters go between vertical bars and the body follows. Types are usually
inferred and usually left out: |x| x * factor is what you would write. A body
of more than one expression takes braces.
What makes closures worth a section is capture. A closure that only reads
factor borrows it. A closure that assigns to a captured variable borrows it
mutably, and must itself be declared mut:
#![allow(unused)]
fn main() {
let mut calls = 0;
let mut counted = |x: i32| {
calls += 1;
x * 2
};
counted(1);
counted(2);
println!("called {calls} times");
}
A closure marked move takes ownership of everything it captures, needed when it
has to outlive the scope that made it, such as a closure handed
to another thread or returned from a function:
#![allow(unused)]
fn main() {
let name = String::from("world");
let greet = move || println!("hello, {name}");
greet();
}
After that move, name belongs to the closure and the surrounding code cannot
use it. The compiler decides borrow or borrow-mutably on its own; move is the
one part you say out loud.
4.5 Passing Functions Around
A function that takes a closure declares what it needs to do with it. Three
traits describe the choices, and they nest: Fn can be called repeatedly and
only reads what it captured, FnMut can be called repeatedly and may modify
what it captured, and FnOnce can be called once because calling it consumes
something. Take the least demanding one that works, nearly always Fn.
Here is bisection, which takes any function from f64 to f64:
// Find a root of f between lo and hi, where f(lo) and f(hi) have opposite signs.
fn bisect(f: impl Fn(f64) -> f64, mut lo: f64, mut hi: f64) -> f64 {
for _ in 0..60 {
let mid = (lo + hi) / 2.0;
if f(lo) * f(mid) <= 0.0 {
hi = mid;
} else {
lo = mid;
}
}
(lo + hi) / 2.0
}
fn main() {
let root = bisect(|x| x * x - 2.0, 0.0, 2.0);
println!("sqrt(2) = {root:.10}");
let target = 10.0;
let root = bisect(move |x| x * x * x - target, 0.0, 10.0);
println!("cbrt(10) = {root:.10}");
}
sqrt(2) = 1.4142135624
cbrt(10) = 2.1544346900
impl Fn(f64) -> f64 in a parameter position means “some type that can be
called this way”, decided at compile time. The closure is compiled into the
function directly, so this costs nothing over writing the arithmetic inline. The
same notation in the return position means “some type I am not naming”, and is
how you return a closure:
#![allow(unused)]
fn main() {
fn make_adder(n: i32) -> impl Fn(i32) -> i32 {
move |x| x + n
}
}
The move is required. Without it the closure would borrow n, and n is gone
the moment the function returns.
A plain function can be passed wherever a closure is expected, and fn(i32) -> i32 is the type of a function that captures nothing:
fn map_all(values: &[i32], f: fn(i32) -> i32) -> Vec<i32> {
let mut out = Vec::new();
for &v in values {
out.push(f(v));
}
out
}
fn double(n: i32) -> i32 {
n * 2
}
fn main() {
let values = [1, 2, 3, 4];
println!("{:?}", map_all(&values, double));
println!("{:?}", map_all(&values, |n| n * n));
}
[2, 4, 6, 8]
[1, 4, 9, 16]
Note for &v in values: the loop over a slice produces references, and the &
in the pattern takes the value out of the reference. Writing for v in values
and then f(*v) says the same thing. Both forms are everywhere in real code.
Closures are how the standard library is meant to be used. Sorting takes one:
#![allow(unused)]
fn main() {
let mut words = vec!["pear", "fig", "banana", "kiwi"];
words.sort_by_key(|w| w.len());
println!("{words:?}");
}
["fig", "pear", "kiwi", "banana"]
sort_by_key sorts by whatever the closure computes. The sort is stable, so
pear and kiwi, both of length four, keep the order they came in with.
sort_by takes a two-argument comparison instead, for when there is no single
key. {:?} is the debugging format, which prints structures rather than
requiring them to be presentable; Chapter 9 explains where it comes from.
Exercise 4-1. Write power from Chapter 1 as a recursive function that
squares its way down, so that power(2, 10) performs four multiplications
rather than ten.
Exercise 4-2. Write apply_twice(f, x) that returns f(f(x)). What is its
signature?
Exercise 4-3. Use bisect to find where cos(x) - x is zero, and check the
answer against a calculator.
Exercise 4-4. bisect calls f(lo) on every round even though lo usually
has not changed. Rewrite it to call f once per iteration.
Exercise 4-5. Write a function that takes a slice of words and returns the longest, and decide what it should do when two are tied. What type should it return when the slice is empty?