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:
If you find this helpful, please like, bookmark, and follow. To keep learning along, follow this series.
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
}
This is the version using a closure:
let expensive_closure = |num:u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
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));
}
}
}
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
.
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 ;
{}
. Because it contains only one expression, the braces can be omitted.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);
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
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.