Rust fn and closure and Fn traits

Jeff posted on  (updated on )

This article shows you the following concepts in Rust programming language:

  1. fn
  2. Fn/FnMut/FnOnce
  3. Closure

fn

fn is a primitive type in Rust, it represents a function pointer.

See https://doc.rust-lang.org/std/primitive.fn.html

FnOnce/FnMut/Fn traits

There are 3 different function traits in Rust. They are mainly used when working with closures. We will talk about closure in the next section, lets first understand what does each trait define.

https://doc.rust-lang.org/book/ch13-01-closures.html#moving-captured-values-out-of-closures-and-the-fn-traits

  1. FnOnce applies to closures that can be called once. All closures implement at least this trait, because all closures can be called. A closure that moves captured values out of its body will only implement FnOnce and none of the other Fn traits, because it can only be called once.
  2. FnMut applies to closures that don’t move captured values out of their body, but that might mutate the captured values. These closures can be called more than once.
  3. Fn applies to closures that don’t move captured values out of their body and that don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.

Or, represented in table

Move captured value out of bodyMutate captured value
FnOnceYes-
FnMutNoYes
FnNoNo

Don't worry if you don't full understand them, we will cover them in more details below.

Closure

Now lets talk about closures. From a developer's point of view, closures are anonymous functions, its similar to lambda function in other programming languages.

Why we need closure when we have functions? Unlike functions, closures can capture values from the scope in which they’re defined.

The syntax to create closures in Rust is

let name_of_the_closure = |arg1: Type1, arg2: Type2| -> ReturnType {
    // Do something within closure
};

-- or --
// you can alter the behavior of capturing values by using move keyword
//                        |
//                        v
let name_of_the_closure = move |arg1: Type1, arg2: Typ2| -> ReturnType {
    // Do something within closure
};

There are 2 import aspects of closure that need attention

  1. How it captures the values from outside
  2. How it interacts with captured values

How it captures the values from outside

Inside a normal function, we can only work with variables defined in the function signature (parameters). Closures not only can work with its parameters, but also any variables from the scope in which the closure is defined.

let mut vector: Vec<i32> = vec![1];
let closure = || {
	// vector here is captured from outside implicitly
	vector.push(2);
};

In above example, if we run the closure then print the vector, we will see an output of [1, 2].

Closure can capture values from their environment in three ways

  1. Borrowing immutably
  2. Borrowing mutably
  3. Taking ownership

Very similar to how function can take a parameter

The Rust compiler will automatically figure out which method to use, given the code inside the closure.

// 1. Borrowing immutably
let vector: Vec<i32> = vec![1];
let closure = || {
	println!("{:?}", vector);
};

// 2. Borrowing mutably
let mut vector: Vec<i32> = vec![1];
let closure = || {
	vector.push(2);
};

// 3. Taking ownership
let vector: Vec<i32> = vec![1];
let closure = || {
	some_func_that_takes_ownership(vector);
};

For the third option of taking ownership, we can force it to happen by using move keyword

// 3. Taking ownership forcefully
let vector: Vec<i32> = vec![1];
let closure = move || {
	// Even our code doesn't require taking ownership to work
	// The move keyword forces it
	println!("{:?}", vector);
};
// print!("{:?}", vector); <--- not possible

move converts any variables captured by reference or mutable reference to variables captured by value.

This move keyword is usually used when creating threads, see more here

https://doc.rust-lang.org/book/ch13-01-closures.html#capturing-references-or-moving-ownership

How it interacts with captured values

Now that we capture some values inside the closure, we can manipulate them in many ways. The 2 most important things we can do here is

  1. Mutate the captured values
  2. Move the captured values out of closure body (transfer ownership)

This part is important, as different behavior here will result in different trait (Fn/FnMut/FnOnce) being implemented by the closure. The closure implements 3 traits in an additive fashion, that means, all closures impl FnOnce, and if it mutates captured values but doesn't move captured values, it now impl FnOnce + FnMut, and if it doesn't mutate or move, it now impl FnOnce + FnMut + Fn

In other words, a closure will implement more traits if it interacts less with captured values.

Here's a graph to represent the scope of each trait closure traits This means

  1. We can use Fn closure as if its either Fn/FnMut/FnOnce
  2. We can use FnMut closure as if its either FnMut/FnOnce
  3. FnOnce closure is the most restrictive, can only be used as FnOnce

Since bigger scope covers smaller one, you only need to specify the biggest scope you want to define for a closure i.e. No need to type Fn() + FnMut() + FnOnce(), just Fn() is enough

How is it determined?

Like what we covered in section "How it captures the values from outside", Rust compiler will automatically assign traits to a closure, given how it interacts with captured values.

Here's the rule, as shown earlier

Move captured value out of bodyMutate captured value
FnOnceYes-
FnMutNoYes
FnNoNo

Some code example

let vector: Vec<i32> = vec![1];
// FnOnce, it moves captured value out of body
let fn_impl_FnOnce = || {
	// vector captured via taking ownership
	some_func_that_takes_ownership(vector);
};
let mut vector: Vec<i32> = vec![1];
// FnMut, it mutates captured value
let fn_impl_FnMut = || {
	// vector captured via borrowing mutably
	// captured value is mutated
	vector.push(2);
};
let vector: Vec<i32> = vec![1];
// Fn, it doesn't mutate or move captured value
let fn_impl_Fn = || {
	// vector captured via borrowing immutably
	println!("{:?}", vector);
};

Type coercion

You can pass a closure that implements Fn to a function that accepts fn() (and vice versa) because the Rust compiler performs automatic type coercion for stateless closures.

Reference