Ambiguous behavior in iterator while using the find/filter methods

I don’t think I’ve seen this behavior before while using iterators. I have the following program.

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::<String, String>::new();
    map.insert("Hello".to_owned(), "World".to_owned());
    map
        .iter()
        .find(|(&k, &v)| k == "Hello")
        .unwrap();
}

It gives me following error -

error[E0507]: cannot move out of a shared reference
 --> src/main.rs:8:16
  |
8 |         .find(|(&k, &v)| k == "Hello")
  |                ^^-^^^-^
  |                  |   |
  |                  |   ...and here
  |                  data moved here
  |
  = note: move occurs because these variables have types that don't implement the `Copy` trait

My question is why? The closure in find takes a &Self::Item therefore the iterator item is (&String, &String), which when used with the find function is actually (&&String, &&String). Me using the pattern (&k, &v) should move a string reference into k and v and not the actual strings themselves. So how am I moving out of a shared reference?

Another possibility is that the iterator item in find is actually &(&String, &String). But that should only happen when I explicitly place that & outside the (&String, &String). What is happening when I don't use that outer & explicitly?

It's &(&String, &String) because it doesn't matter where you put the &: it's &Self::Item, and Self::Item is (&String, &String), so almost copy-pasted we end with &(&String, &String).

1 Like

No, if Item = (&String, &String), then of course &Item = &(&String, &String). There's no magic here, types are compositional, so you should not expect the compiler to automatically "look through" composite types and change them in arbitrary ways.

1 Like

So if we have -

(1) let items: (&i32, &i32) = (&45, &24);
(2) let (a, b): &(&i32, &i32) = &items;

Something has to happen to the outer & in &(&i32, &i32 on (2). Right now it seems like the compiler just doesn't use it.

Well that's a different story; it's a confusing feature called "pattern matching ergonomics". It basically means that sometimes, the compiler does try to adjust the types of tuple and struct fields so that the pattern can be matched even if the types don't line up perfectly. I suggest until you understand Rust's type system completely, you turn it off in Clippy.

What you are seeing is the effect of "match ergonomics". To make pattern matching cleaner and remove lots of &s and refs when doing things like match statements, the compiler will let you elide certain parts of the pattern and automatically rewrite something like (x, y) as &(&x, &y).

Thanks for the link to the RFC! I think it makes a lot of sense now. I think there may be a bug here -

    let items: (&i32, &i32) = (&45, &24);
    let (a, b) = &items; // a & b are &i32
    let _: &i32 = a;  // Works
    let _:  i32 = *a; // Errors
    let _:  i32 = **a; // Works. Is this suggesting a is &&i32? Why?

Actually, a and b are both &&i32. Match ergonomics effectively allow for an &(T, U) -> (&T, &U) conversion. Here, items of type &(&i32, &i32) becomes (a, b) of type (&&i32, &&i32). To get the expected behavior, you can use let &(a, b) = &items;, which dereferences &items.

2 Likes

@H2CO3 made a statement that contradicts this.

I don't see how our statements contradict. Although I suppose I was somewhat inaccurate: with match ergonomics, the compiler automatically dereferences the outer references, then forces all the bindings into ref [mut] bindings; the &(T, U) is never directly converted into a (&T, &U). But the difference is only relevant when trying to use it with reference patterns. Consider the following example. The 1st line is equivalent to the 2nd due to ergonomics. But the 3rd line dereferences the &i32 values, not the &&i32 values, since the double references are only a result of the ref bindings. Because of this effect, the 4th line produces an error.

let (a, b) = &items;         // (a, b): (&&i32, &&i32)
let (ref a, ref b) = &items; // (a, b): (&&i32, &&i32)
let (&a, &b) = &items;       // (a, b): (i32, i32)
let (&&a, &&b) = &items;     // error: expected `i32`, found reference

The interaction between match ergonomics, reference patterns, and ref [mut] bindings can definitely be unintuitive at times. I've personally gotten myself confused more than once writing the argument list for a FnMut(&(T, U)) closure (e.g., a filter following an enumerate).

1 Like

It's inconsistent (broken) to boot. Also, reader beware, the implementation does not match the RFC in other ways too.

1 Like

Asked the same question on StackOverflow and I think I got a satisfactory answer there.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.