3

The following compiles and works as expected:

// Example 1
for i in (0..10).filter(|i| i % 2 == 0) { println!("{} is even", i); }

I was expecting the following to work, too:

// Example 2
let is_even = |i| i % 2 == 0;
for i in (0..10).filter(is_even) { println!("{} is even", i); }

However, it fails to compile:

error[E0308]: mismatched types
  --> src/main.rs:14:14
   |
14 |     for i in (0..10).filter(is_even) { println!("{} is even", i); }
   |              ^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected type `for<'r> std::ops::FnMut<(&'r i32,)>`
              found type `std::ops::FnMut<(&i32,)>`

The compiler complains that the argument of my closure has the wrong lifetime, the compiler expects a higher-rank trait bound (HRTB).

The reason is given in this answer:

Ultimately, this is caused due to limitations in Rust's type inference. Specifically, if a closure is passed immediately to a function that uses it, the compiler can infer what the argument and return types are. Unfortunately, when it is stored in a variable before being used, the compiler does not perform the same level of inference.

Hence, in theory there should probably not be much of a difference between Example 1 and Example 2, but in practice there is (due to technical limitations of the compiler).

Interestingly, the following code compiles and works correctly:

// Example 3
let is_even = |i: &i32| i % 2 == 0;
for i in (0..10).filter(is_even) { println!("{} is even", i); }

However, I don't understand how Example 3, which doesn't contain any explicit lifetime declarations, solves the lifetime issues the compiler complained about in Example 2.

In an answer to another question, the following explanation was given:

By explicitly marking the closure's single input parameter as a reference, the closure will then no longer generalize over the parameter type T, but over a higher-ranked lifetime of the reference input 'a in &'a _.

However, simply marking the input parameter as a reference does not solve the issue, since the following code triggers the same compiler error:

// Example 4
let is_even = |&i| i % 2 == 0;
for i in (0..10).filter(is_even) { println!("{} is even", i); }

What's the precise difference of specifying &i vs. i: &i32 here w.r.t. to the lifetime of the closure parameter?

I'm using rustc 1.46.0.

Florian Brucker
  • 9,621
  • 3
  • 48
  • 81
  • 1
    See also: https://stackoverflow.com/q/58773989 https://stackoverflow.com/questions/63916821 – E_net4 Oct 23 '20 at 16:32
  • @E_net4hasfewfriends: Thanks for your feedback! I've edited the question to make it more precise based on what I've learned from your links. – Florian Brucker Oct 26 '20 at 13:34
  • 1
    The follow-up question is answered by https://stackoverflow.com/q/63916821 (that's the last link I commented above): in example 2, the compiler tries to generalize the closure for parameters of type `T`, but without a HRTB lifetime constraint on `T`; in example 3, the compiler knows to generalize the closure's parameter over an arbitrary lifetime `'a` in `|i: &'a _| i % 2 == 0`. – E_net4 Oct 26 '20 at 15:51
  • @E_net4hasfewfriends: Thanks again, I must have missed that part. However, I don't think the answer of yours that you linked to fully answers my question, because it only talks about declaring the closure parameter a reference, which doesn't seem to be enough in my case. I've updated my question once again to make this clearer. Thanks for your patience! – Florian Brucker Oct 26 '20 at 17:32
  • Placing `&` before the parameter name only performs pattern matching for dereferencing the value, and does not contribute to type inference. The closure `|&i| {}` still expands to `|&i: _| {}`, so example 4 is the same as example 2 in this regard. In the end, it's not worth trying to reason with these gritty details, because even those could change to become more flexible in the future, as hinted in the linked answer. – E_net4 Oct 26 '20 at 19:06
  • @E_net4hasfriends: that was an important detail for me, thank you for your patience! Coming from more mature languages, I guess I still have to get used to Rust evolving more quickly (and I don't mean that in a bad way). – Florian Brucker Oct 27 '20 at 07:11

0 Answers0