12.2 Using Closures
12.2.1 Calling Closures
Closures are called using parentheses ()
, just like functions.
Example:
#![allow(unused)] fn main() { let greet = |name| println!("Hello, {}!", name); greet("Alice"); // Output: Hello, Alice! }
- Even though closures are defined differently, they are invoked similarly to functions.
12.2.2 Closures with Type Inference
Rust's type inference allows you to write closures without explicit type annotations. This can make your code more concise, but it's important to understand how type inference works, as it may lead to some unexpected restrictions.
Example:
#![allow(unused)] fn main() { let add_one = |x| x + 1; let result = add_one(5); println!("Result: {}", result); // Output: Result: 6 }
- The closure
add_one
does not specify the type ofx
or the return type. - The compiler infers the type of
x
based on the usage within the closure and the first call toadd_one(5)
.- Since
5
is an integer literal,x
is inferred to bei32
. - The expression
x + 1
uses the+
operator, which requires both operands to be of the same type.
- Since
- As a result,
add_one
is inferred to be of typeFn(i32) -> i32
.
Important Note on Type Inference and Limitations
While type inference can make code more concise, it can also introduce limitations that might be surprising.
Attempting to Call the Closure with a Different Type:
let res2 = add_one(5.0);
// Error: expected integer, found floating-point number
- Explanation:
- The closure
add_one
has been inferred to take ani32
as its parameter. - Attempting to call
add_one(5.0)
passes af64
(floating-point number), which does not match the expected typei32
. - The compiler will produce an error because the types are mismatched.
- The closure
Why Does This Happen?
- Type Inference Based on First Usage:
- Rust infers types based on how the closure is used when it's first defined or called.
- In our example, the first call
add_one(5)
causesx
to be inferred asi32
.
- Types Become Fixed After Inference:
- Once the types are inferred, they become fixed for the closure.
- Subsequent calls to the closure must use the same types.
How to Allow the Closure to Accept Multiple Types
If you want the closure to accept multiple numeric types, you can:
-
Specify Type Annotations:
#![allow(unused)] fn main() { let add_one = |x: f64| x + 1.0; let result = add_one(5.0); println!("Result: {}", result); // Output: Result: 6.0 }
- Here, we explicitly annotate
x
asf64
. - Now,
add_one
acceptsf64
values. - However, it still won't accept
i32
values without a type conversion.
- Here, we explicitly annotate
-
Use Generics and Traits:
If you need the closure to work with multiple numeric types, you can define a generic function instead of a closure:
use std::ops::Add; fn add_one<T>(x: T) -> T where T: Add<Output = T> + From<u8>, { x + T::from(1) } fn main() { let result_int = add_one(5); let result_float = add_one(5.0); println!("Result int: {}", result_int); // Output: Result int: 6 println!("Result float: {}", result_float); // Output: Result float: 6.0 }
- This function
add_one
is generic over typeT
. T
must implement theAdd
trait and be constructible from au8
.- Now,
add_one
can accept both integers and floating-point numbers.
- This function
Key Takeaways
-
Type Inference in Closures Is Based on Usage:
- The compiler infers types for closures based on how they are used when defined and first called.
- Types become fixed after inference, which can limit how you can use the closure.
-
Explicit Type Annotations Provide Clarity:
- If you anticipate that a closure will need to accept different types, consider adding explicit type annotations.
-
Closures Cannot Be Generic Over Types:
- Closures themselves cannot be generic in the way functions can.
- If you need generic behavior, define a generic function instead.
12.2.3 Closures with Explicit Types
In some cases, you may need to provide type annotations for clarity or to resolve ambiguity.
Example:
#![allow(unused)] fn main() { let multiply = |x: i32, y: i32| -> i32 { x * y }; let result = multiply(6, 7); println!("Result: {}", result); // Output: Result: 42 }
- Type annotations can be helpful when the compiler cannot infer the types.
12.2.4 Closures Without Parameters
Closures can be defined without parameters, using empty vertical pipes ||
.
Example:
#![allow(unused)] fn main() { let say_hello = || println!("Hello!"); say_hello(); // Output: Hello! }
- Useful for closures that act as callbacks or perform an action without needing input.