Yes, using graphemes (borrows of the lowercased strings) makes re-using the HashMap much more complicated. I'd probably skip it for find another approach. But perhaps talking about it will be useful from a learning perspective...
Think of Rust lifetimes ('a things) as the duration of some borrow. Or perhaps in a more refined sense, as points in the control flow of your program where something (or things) are borrowed. So long as some place (a variable, a field, ...) is borrowed, there are restrictions on what can happen to that place. For example, you can't move or destruct a place that is borrowed. It's the borrow checker's job to enforce those restrictions.
The problem in this case is easier to see if we unroll the loop.
// Rewritten to make the loop more conventionally obvious
let mut cand_hash_map: HashMap<&str, u32> = HashMap::new();
let mut iter = possible_anagrams
// ...
.filter(|(_, c_lw)| word_lw.ne(c_lw));
// The loop unrolled a bit
if let Some((cand, c_lw_1)) = iter.next() {
letter_count(&mut cand_hash_map, &c_lw_1); // L10
if word_hash_map == cand_hash_map {
out.insert(cand);
}
// L18
} // L20
if let Some((cand, c_lw_2)) = iter.next() {
letter_count(&mut cand_hash_map, &c_lw_2); // L30
if word_hash_map == cand_hash_map {
out.insert(cand);
}
} // L40
cand_hash_map is a HashMap<&'g str, u32>, so for any s: &str you insert as a key, *s has to be borrowed for (at least) the same duration 'g. Clearly that duration has to include L10 and L30, where you're borrowing c_lw_1 and c_lw_2, and might insert &str from those borrows into the HashMap. In turn that means the borrow from L10 must continue from L10 to at least L30, including L20. But c_lw_1 goes out of scope and gets destructed on L20, which conflicts with being borrowed.
In simpler terms, a borrow of c_lw_1 may still be in the HashMap when you call letter_count on L30, but c_lw_1 has been destructed by that point, so this can't be allowed to compile.
However, note that adding some unconditional .clear()s (like on L18) so that this won't actually happen doesn't fix the borrow checker error. The borrow checker doesn't understand the implementation details of the HashMap; there's no logic like "one cleared, the lifetimes in a HashMap don't matter" it can exploit. You would need some other trick on L18 that the borrow checker accepts as proof that c_lw_1 is no longer borrowed; something rooted in the type system (where the borrow checker operates).
For example, you could have something like this:
// This could be more generic but that would be a distraction.
fn reuse_hashmap(mut map: HashMap<&str, u32>) -> HashMap<&'static str, u32> {
map.clear();
// SAFETY: We are only changing a lifetime which does not effect
// layout and represents borrows held by the key entries in the
// map. We know there are no keys in our map and thus no borrows.
unsafe { std::mem::transmute(map) }
}
And use it like so:
for (cand, c_lw) in iter {
// Due to variance, `this_map` may have an arbitrarily
// short lifetime in its type.
let mut this_map = cand_hash_map;
letter_count(&mut this_map, &c_lw);
if word_hash_map == this_map {
out.insert(cand);
}
// The borrow represented in the type of `this_map` has
// to be active in this call. But after this call,
// `this_map` has been moved and there's nothing that keeps
// that borrow active any more.
cand_hash_map = reuse_hashmap(this_map);
// So here, where `c_lw` is about to drop, the borrow is no
// longer active and there's no conflict with `c_lw` getting
// destructed.
}
Whether it's worth it is another question.
This comes up with Vec<_> sometimes too, where there's a safe sorta-workaround:
// Often optimizes to reuse the existing `vec`, preserving its allocation
vec.into_iter().map(|_| unreachable!()).collect()
I say "sorta" because the optimization is not guaranteed. (I tried the analogous code for HashMap, but it didn't look like it optimized.)