How to define higher-order functions in Rust
Tags: rust, webdev
Published: 2023-02-22 17:32:15

**Written by [Matteo Di Pirro](https://blog.logrocket.com/author/matteodipirro/)✏️**

Higher-order functions (HOFs) are functions whose parameters and/or return values are functions themselves. In other words, if a language supports higher-order functions, then we say that these functions are first-class citizens, that is they are values. 

In this article, we’ll investigate how Rust supports higher-order functions and how we can define them. 

_Jump ahead:_

*   [Functions in Rust](#functions-rust)
*   [Functions as parameters](#functions-parameters)
    *   [Named functions](#named-functions)
    *   [Anonymous functions](#anonymous-functions)
*   [Functions as returned values](#functions-returned-values)

## Functions in Rust <a name="functions-rust">

We can define functions in Rust via the `fn` keyword. As usual, to define a function we have to specify its name, parameters, and the type of the returned value:

<pre class="language-rust hljs">fn plus_one(n: i32) -> i32 {
	n + 1
}</pre>

The `return` keyword is optional. If we don’t specify it, the last statement of the function is considered as the return statement. 

As we said earlier, functions in Rust are first-class citizens. Hence, we can store them in a variable. Once it is stored in a variable, we can invoke it as usual:

<pre class="language-rust hljs">fn main() {
	let add_one = plus_one;

	println!("{}", add_one(1));
}</pre>

## Functions as parameters <a name="functions-parameters">

In the above section, we demonstrated how to define a function and store it in a variable. Now, let’s see how to pass a function as a parameter to another function. 

First, we have to create our higher-order function definition:

<pre class="language-rust hljs">fn binary_operator<F>(n: i32, m: i32, op: F) -> i32 
                     where F: Fn(i32, i32) -> i32 {
	op(n, m)
}</pre>

`binary_operator` inputs two numbers, `n` and `m`, and a function, `op`. It applies `op` to `n` and `m`, returning the result. 

Note the type of the `op` parameter. It is a generic type `F`, refined in the `where` clause of `binary_operator`. In particular, we defined it as a function (`Fn`) with two numeric parameters (`(i32, i32)`), returning a parameter (`i32)`. `Fn` here represents a function pointer, that is, the memory address where the function is stored.

### Named functions

The simplest way to pass a function as a parameter is by using its `name (named function)`:

<pre class="language-rust hljs"> add(n: i32, m: i32) -> i32 {
    n + m
}

fn binary_operator<F>(n: i32, m: i32, op: F) -> i32 
                     where F: Fn(i32, i32) -> i32 {
	op(n, m)
}

fn main() {
	println!("{}", binary_operator(5, 6, add));
}</pre>

In the example above, we first define a function (`add`) to add two numbers and use it as a parameter to `binary_operator`. `main` prints `11`, as expected.

### Anonymous functions

Sometimes it’s not necessary to name functions. For example, we might want to define a function on the fly, to be used only in a single place. This is where anonymous functions come into play:

<pre class="language-rust hljs">fn main() {
	println!("{}", binary_operator(5, 6, |a: i32, b: i32| a - b));
}</pre>

In the example above, we define an anonymous function directly in the call to `binary_operator`. We define the parameters list between pipes (`||`), followed by the body of the function itself. 

Anonymous functions are a very powerful tool, more so because, in Rust, they can “capture” the enclosing environment. In this case, the functions are also called closures. 

The snippet above compiles and prints `-1`, as expected.

## Functions as returned values <a name="functions-returned-values">

As we mentioned earlier, in Rust a function can also return another function. This is a bit more complicated due to the consequence of memory management in Rust, as we’ll see shortly. 

In the following example, we’ll modify `binary_operator`, defined above, not to apply the operator but, instead, to return a function representing an unapplied operator:

```rust
fn unapplied_binary_operator<'a, F>(n:& 'a i32, m:& 'a i32, op:& 'a F) 
                                   -> Box<dyn Fn() -> i32 + 'a>
                                   where F: Fn(i32, i32) -> i32 {
	Box::new(move || op(*n, *m))
}
```

The definition of `unapplied_binary_operator` now looks much more complex. 

The main problem with returning a function is defining the length of that function’s lifetime. A lifetime in Rust is a construct the borrow checker uses to ensure all borrows are valid. 

The borrow checker and, more in general, how borrowing works in Rust is out of the scope of this article. If you’re unfamiliar with the Rust borrow checker, or want to know more, [check out this article](https://blog.logrocket.com/introducing-the-rust-borrow-checker/). 

In the above example, we define a lifetime (`'a`) together with the usual `F` type (representing the binary operator). Then, we associate `'a` to the three parameters as well as to the returned value of the function. 

Basically, we’re telling the borrow checker to consider the lifetime of the function returned by `unapplied_binary_operator` as long as the three parameters (`n`, `m`, and `op`) exist. 

Furthermore, lifetimes in Rust may only exist with references. Hence, we have to turn our parameters and returned value into references with `&` and `Box`, respectively. 

Generally speaking, `Box<dyn Fn()>` represents a boxed-up value implementing the `Fn` trait. 

References make the function body more complex than before, as we now have to dereference the references to `n` and `m`, and explicitly create the resulting function using `Box::new()`. 

Another interesting thing in the above implementation is the use of `move`. This keyword signals that all captures (i.e., all references to the enclosing environment) occur by value. Otherwise, any captures by reference will be dropped as soon as the returned anonymous function exists, leaving the closure with invalid references. In other words, with `move`, the closure takes ownership of the variables it uses. 

`unapplied_binary_operator` returns a function, with no arguments, returning the result of the application of `op` to `n` and `m`. Provided that we’re now using references, we have to use borrowing to invoke it:

```rust
fn main() {
	let n = 5;
	let m = 6;
	println!("{}", unapplied_binary_operator(&n, &m, &add)());
}
```

Note how we borrow `n`, `m`, and `add` (defined above) using `&`. Lastly, since `unapplied_binary_operator` returns a function with no parameters, we can invoke it using empty parentheses. The above snippet will print `11`, as expected.

## Conclusion

In this article, we took a quick dive into higher-order functions in Rust. We demonstrated how to define a simple function. Then, we explored the ways to pass a function as a parameter. Lastly, we investigated how much more complex it is to return a function, briefly mentioning some key Rust features, such as borrowing and lifetimes. 

Higher-order functions are a key feature in Rust and in many other programming languages; HOFs are a [foundational concept of functional programming](https://blog.logrocket.com/optimizing-rust-code-with-closures-environment-capturing/). We can use higher-order functions to write code that is more concise and easier to maintain in the long run.

---
## LogRocket: Full visibility into production Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, [try LogRocket](https://lp.logrocket.com/blg/signup). 

[![LogRocket Dashboard Free Trial Banner](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://lp.logrocket.com/blg/signup)

[LogRocket](https://logrocket.com/signup) is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — [start monitoring for free](https://lp.logrocket.com/blg/signup).