DEV Community

Cover image for [Rust Guide] 13.2. Closure Type Inference and Annotations
SomeB1oody
SomeB1oody

Posted on

[Rust Guide] 13.2. Closure Type Inference and Annotations

13.2.0 Before We Begin

During its design, Rust drew inspiration from many languages, and functional programming had a particularly strong influence on Rust. Functional programming often includes passing functions as values to parameters, returning them from other functions, assigning them to variables for later execution, and so on.

In this chapter, we will discuss some Rust features that are similar to what many languages call functional features:

  • Closures (this article)
  • Iterators
  • Improving the I/O Project with Closures and Iterators
  • Performance of Closures and Iterators

If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.

13.2.1 Type Inference for Closures

Unlike functions defined with fn, closures do not require explicit type annotations for parameters or return values.

Functions must be explicit because they are part of a public interface exposed to users, and a clearly defined interface helps everyone agree on the parameter and return types.

Closures are not used as exposed interfaces. They are usually stored in variables, they do not need names when used, and they are not exposed to users of our codebase. Therefore, closures do not require explicit type annotations for parameters and return values.

Closures are also usually short and work only in a narrow context, so the compiler can often infer the types. Of course, you can still write the annotations manually if you want to.

Take a look at an example:
This is the version using a function definition:

fn simulated_expensive_calculation(intensity: u32) -> u32 {  
    println!("calculating slowly...");  
    thread::sleep(Duration::from_secs(2));  
    intensity  
}
Enter fullscreen mode Exit fullscreen mode

This is the version using a closure:

let expensive_closure = |num:u32| -> u32 {  
    println!("calculating slowly...");  
    thread::sleep(Duration::from_secs(2));  
    num  
};
Enter fullscreen mode Exit fullscreen mode

Explicit annotations are used here because there is no surrounding context for Rust to infer the types. If there is context, then they are not needed:

fn generate_workout(intensity: u32, random_number: u32) {  
    let expensive_closure = |num| {  
        println!("calculating slowly...");  
        thread::sleep(Duration::from_secs(2));  
        num  
    };  
    if intensity < 25 {  
        println!("Today, do {} pushups!", expensive_closure(intensity));  
        println!("Next, do {} situps!", expensive_closure(intensity));  
    } else {  
        if random_number == 3 {  
            println!("Take a break today! Remember to stay hydrated!");  
        } else {  
            println!("Today, run for {} minutes!", expensive_closure(intensity));  
        }  
    }  
}
Enter fullscreen mode Exit fullscreen mode

The parameter num does not need an explicit type annotation because the argument passed in the later call is intensity, whose type is u32, so Rust infers that num is also u32.

13.2.2 Syntax for Function and Closure Definitions

Here are four examples:

 fn add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;
Enter fullscreen mode Exit fullscreen mode
  • The first is a function definition, with a function name, parameter names and types, and a return type
  • The second is a closure definition, with parameter and return types. This closure looks very similar to a function definition.
  • The third is also a closure, but its parameter and return types are not annotated, so the compiler has to infer them.
  • The fourth closure differs from the third in that it does not use curly braces {}. Because it contains only one expression, the braces can be omitted.

13.2.3 Type Inference for Closures

A closure’s definition will ultimately infer only one specific concrete type for its parameters and return value.

Take a look at an example:

    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
Enter fullscreen mode Exit fullscreen mode

Output:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^- help: try using a conversion method: `.to_string()`
  |             |               |
  |             |               expected `String`, found integer
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^

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

When the compiler sees the first call to the closure, it determines that both the input and output values are String, so it locks in String as the parameter and return type for this closure. That is why a later call with an integer causes an error.

Top comments (0)