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.