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 of x or the return type.
  • The compiler infers the type of x based on the usage within the closure and the first call to add_one(5).
    • Since 5 is an integer literal, x is inferred to be i32.
    • The expression x + 1 uses the + operator, which requires both operands to be of the same type.
  • As a result, add_one is inferred to be of type Fn(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 an i32 as its parameter.
    • Attempting to call add_one(5.0) passes a f64 (floating-point number), which does not match the expected type i32.
    • The compiler will produce an error because the types are mismatched.

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) causes x to be inferred as i32.
  • 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:

  1. 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 as f64.
    • Now, add_one accepts f64 values.
    • However, it still won't accept i32 values without a type conversion.
  2. 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 type T.
    • T must implement the Add trait and be constructible from a u8.
    • Now, add_one can accept both integers and floating-point numbers.

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.