DEV Community

Cover image for [Rust Guide] 19.3 Advanced Functions and Closures
SomeB1oody
SomeB1oody

Posted on • Originally published at someb1oody.github.io

[Rust Guide] 19.3 Advanced Functions and Closures

19.3.1 Function Pointers

We have already talked about passing closures into functions. In fact, we can also pass functions into functions.

When they are passed around, functions are coerced into the fn type, which is a function pointer.

For example:

fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}
Enter fullscreen mode Exit fullscreen mode

The first parameter of do_twice, f, is of type fn, which means it is a function pointer. It expects a function whose parameter type is i32 and whose return type is also i32. The function body calls f twice.

Output:

The answer is: 12
Enter fullscreen mode Exit fullscreen mode

The Difference Between Function Pointers and Closures

At minimum, closures implement one of the Fn, FnOnce, and FnMut traits. A function pointer fn is a type, not a trait. We can specify fn directly as a parameter type without declaring a generic parameter bounded by the Fn traits.

Function pointers implement all three closure traits, namely Fn, FnOnce, and FnMut. So you can always pass a function pointer as an argument to a function that accepts a closure. That is why we usually prefer generic parameters with closure traits when writing functions, because then the function can accept both closures and ordinary functions.


In some cases, we may want to accept an fn type instead of a closure, for example when interacting with code that does not support closures, such as C functions. How do we write that?

Here is an example:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> = list_of_numbers
        .iter()
        .map(|i| i.to_string())
        .collect();
    // Splitting the lines is just for readability; it is not required
}
Enter fullscreen mode Exit fullscreen mode

The elements in list_of_numbers are i32, and we want to convert them into String values for list_of_strings. The steps are:

  • First, use iter to produce an iterator
  • Then use the closure inside map, |i| i.to_string(), to convert each element
  • Finally, use collect to gather all the converted elements into a collection

This code can also be written like this:

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> = list_of_numbers
        .iter()
        .map(ToString::to_string)
        .collect();
}
Enter fullscreen mode Exit fullscreen mode

The difference is .map(ToString::to_string), where we pass the to_string function directly. The effect is the same as the previous version. By the way, ToString::to_string uses the fully qualified syntax discussed in 19.2. Advanced Traits.

Let’s look at the definition of map:

fn map<B, F>(self, f: F) -> Map<Self, F>
where
    Self: Sized,
    F: FnMut(Self::Item) -> B
Enter fullscreen mode Exit fullscreen mode

map requires f to implement the FnMut trait, and both closures and function pointers satisfy that requirement, so either can be passed in.

Here is another example:

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20)
        .map(Status::Value)
        .collect();
}
Enter fullscreen mode Exit fullscreen mode

Pay attention to the argument to map. We use the constructor Status::Value to call map on each u32 in the range and create Status::Value instances.

Someone might ask: isn’t Status::Value an enum variant? How did it become a function? That is because in Rust, constructors like this are implemented as functions that take one argument and return a new instance. In other words:

let v = Status::Value(3);
Enter fullscreen mode Exit fullscreen mode

This is just an example. Here, v is initialized, and Status::Value(3) can be viewed as a constructor call: 3 is the constructor’s argument. Since constructors are implemented as functions, we can treat them as functions, with 3 as their argument.

So we can also use such constructors as function pointers that implement closure traits.

19.3.2 Returning Closures

Closures are expressed through traits, so you cannot directly return a closure from a function. Instead, you can return a concrete type that implements the trait.

For example:

fn returns_closure() -> dyn Fn(i32) -> i32 {
    |x| x + 1
}
Enter fullscreen mode Exit fullscreen mode

This function tries to return a closure directly.

Output:

error[E0746]: return type cannot be a trait object without pointer indirection
 --> src/lib.rs:1:25
  |
1 | fn returns_closure() -> dyn Fn(i32) -> i32 {
  |                         ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time
  |
help: consider returning an `impl Trait` instead of a `dyn Trait`
  |
1 - fn returns_closure() -> dyn Fn(i32) -> i32 {
1 + fn returns_closure() -> impl Fn(i32) -> i32 {
  |
help: alternatively, box the return type, and wrap all of the returned values in `Box::new`
  |
1 ~ fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
2 ~     Box::new(|x| x + 1)
  |

For more information about this error, try `rustc --explain E0746`.
error: could not compile `functions-example` (lib) due to 1 previous error
Enter fullscreen mode Exit fullscreen mode

Rust does not know how much space it needs to store the closure, so it reports an error.

Remember where we ran into the same “Rust does not know how much space to allocate” error before? Yes—when learning about linked lists. The solution there was to wrap the list in Box<T>, and we can do the same here:

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}
Enter fullscreen mode Exit fullscreen mode

Because the return value is behind a pointer, the return type now has a size known at compile time.

Top comments (0)