13 February 2023
Functions are a pretty basic feature of Rust and I’ve obviously been using them already. But there’s still some details that I want to go over before moving on to methods and closures.
Here’s a basic function:
fn add(a: i32, b: i32) -> i32 {
a + b
}
A function definition is introduced by the fn
keyword. It has a name, a list of
parameters, each with a type, a return type, and a body that’s executed when the function is called.
The body consists of a list of statements separated by semicolons. The return value is either the
value of the last statement or a value returned explicitly with a return
statement.
You always have to provide the types for parameters and return values since type inference in Rust is always limited to a single function.
If you’ve used Go before you may be wondering if we can define the function as add(a, b:
i32)
and avoid repeating the parameter type. The answer is no: in Rust you always have to
spell out the types. I find that a bit unfortunate -- it’s a minor detail of course, but it seems
like a nice touch that Go doesn’t make you slavishly repeat the type.
You can define a function within another function, but unlike in Python or Go, the nested function doesn’t have access to the outer function’s arguments or local variables. The only benefit of a nested function is limiting its scope.
If a function “doesn’t return anything” then its return type is the unit type ()
. In
that case you can also leave out the return type, and that is indeed the idiomatic way to write it.
The unit type has exactly one value. What if there are zero possible return values? If you have a
function that never returns then its return type should be the empty type !
as in this
example:
fn panic_with_oops() -> ! {
panic!("oops!");
}
The main
function can have any return type that implements the Termination trait.
Other than ()
the most common choice is Result<(), E>
, where
E
implements Debug
. The way this works is that on Ok
your
program exits with a “success” exit code; on Err
it’ll print the error and exit with a
“failure” exit code. You can use that for simple error handling for command-line programs.
Functions can be passed around as function pointers. A function pointer type is written with
the fn
keyword, like a function definition without the function name and body. The
following example uses a function pointer to modify each value in a tuple:
fn apply(f: fn(f64) -> f64, value: (f64, f64, f64)) -> (f64, f64, f64) {
let (x, y, z) = value;
(f(x), f(y), f(z))
}
Function pointer types aren’t used much in practice because the Fn
, FnMut
,
and FnOnce
types from the std::ops module can do the same job
and they also work for methods and closures.