Rust: Functions

Home · Blog

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.