Caveat upfront: These are things not really documented that well, when at all, and on the whole subject to some amount of change. Inference in general is a tricky area because small changes can be breaking in unexpected ways on top of that.
Also this reply (like this thread) is sort of all over the place, sorry about that.
Some more background on functions being late-bound vs early-bound, higher-ranked vs. not:
One of the take-aways is that these functions are higher-ranked:
fn f(_: &u8) {}
fn g<'a>(_: &'a u8) {}
But this one is not, because the bound -- even though otherwise meaningless! -- opts the function out of being higher-ranked:
// The bound -- ':' -- means this is sugar for a where-clause
// `where 'a: 'a`
fn h<'a: 'a>(_: &'a u8) {}
So that's how you can tell the higher-ranked-or-not nature of functions.
Unfortunately, the compiler errors aren't quite nuanced enough here -- it says things like
28 | let c: fn(&_) = h;
| ^ one type is more general than the other
|
= note: expected fn pointer `for<'r> fn(&'r u8)`
found fn pointer `fn(&u8)`
Really it found the fn(&'x u8)
for some concrete 'x
, like in the quiz; fn(&u8)
is short for for<'a> fn(&'a u8)
. (Granted, there is no handy syntax for the type when the lifetime is anonymous yet concrete like that...)
Also keep the subtyping in mind -- a higher-ranked for<'a> fn(&'a u8)
can coerce into a fn(&'specific u8)
. And there are some other nuances in the weeds I'll just ignore here.
However, as it turns out, the leak coherence lint is about implementations, and it doesn't consider bounds -- only naming. So in the example provided before, these are both considered overlapping:
// Overlaps `impl<T> Trait for fn(T)
impl<'a, T: 'a> Trait for fn(&'a T) {}
impl<'a, T> Trait for fn(&'a T) {}
Even though there's no (non-implicit) bound involving 'a
in the second one. Just being able to name 'a
is enough. Why the difference? This part is even more a guess than the rest of this accumulated knowledge, but I think it's a combination of:
- The lint just isn't smart enough
- Even having a name may make it too tricky to detect problematic cases, due to implicit bounds
Well, that's the second time I've mentioned implicit bounds without defining it, so what am I talking about? I'm referring to this:
&'a T
implying that T: 'a
. Sometimes this is also called a WF
bound or such, which is short for "well-formed". Having a &'a T
where T: 'a
does not hold is undefined behavior, so it's generally just implied to hold. This is certainly great just for having to type less boilerplate, but also has practical (if convoluted) uses. But also non-convoluted uses -- remember that if you write out this implied bound on your function definitions, they will no longer be higher-ranked!
Now, looking at the leak coherence lint a little closer...
Eh, I wouldn't call it bad exactly, albeit unintuitive. Here's a bad pair:
trait Trait {}
impl Trait for for<'a, 'b> fn(&'a u32, &'b u32) {}
impl Trait for for<'c> fn(&'c u32, &'c u32) {}
Like the issue says, that used to warn as part of the same lint, but is now rejected -- those fn
are subtypes of each other, i.e. the same type. However, from that same issue,
There will still be impls that trigger #56105 after this lands, however -- I hope that we will eventually just accept those impls without warning, for the most part. One example of such an impl is this pattern
trait Trait {}
impl<T> Trait for fn(&T) { }
impl<T> Trait for fn(T) { } // still accepted, but warns
Which is to say, unless the direction of Rust changes (again), when you get a leak coherence lint error, it's probably on something that is hopefully accepted some day. See also this writeup for MCP 295.
More conversation from the issue:
[...] they would be considered distinct types. In particular, the first type fn(T)
cannot ever be equal to the second type for<'a> fn(&'a U)
because there is no value of T
that can name 'a
.
So that's why they're non-overlapping: they're distinct types (although the latter is a subtype of the former).
This is an area where the direction of Rust has changed before though, if you read through all those issues.
The reason for the warning, however, is that we cannot yet distinguish that type from those where implied bounds lets us figure out that 'a
is equal to some other region that T
could name (I give some examples in the MCP, I think).
And this is the source of my guess on why the lint is fuzzier in terms of knowing when it will trigger than function definitions are in terms of knowing when they are higher-ranked or not.
(Probably if I reread the MCP in depth, I would regain a more solid understanding.)
So, what's the connection to closures? Closures can be higher-ranked over lifetimes too, but (unless/until RFC 3216 is implemented), you're generally relying on inference to make it that way. And it's not great at it really; hence the RFC for a better concrete way to force higher-rankedness. Adding a arg: &_
to the definition can nudge the compiler in that direction, currently. But there are no hard guarantees around it; the main thing preserving behavior is probably just a desire not to break things by changing inference too much.
Thus it's even harder to identify a higher-ranked closure on sight: It depends on both its context and how inference behaves, which is a lot fuzzier to me at least than the rules around function declarations; certainly it's a lot touchier with regards to things like refactoring. The RFC would definitely help.
(My personal take is that we should also gain a way to annotate non-higher-ranked lifetime parameters in closures, as that would open the way to making anonymous lifetimes '_
higher-ranked in closure parameter lists, just like they are in fn
declarations. Then in like 4 editions or whatever we could eventually have better closure inference by default, as there would be better ways to mitigate inference breakage by being more explicit.)
((Technically I don't think the behavior with function declarations is guaranteed either , but it's much more concrete than inference, and a lot of things would break if it changed -- making less things higher-ranked is a big breaking change, and making turbofishable functions higher-ranked would make them no-longer-turbofishable, which is also a breaking change.))