Rust Quiz 5: when will higher-ranked closure type be yielded?

Just an up-front note that I'm not advocating a position, just conveying my understanding of the situation.

I think it's analogous to why this doesn't work:

trait Trait {}
impl<T: ?Sized> Trait for T {}

fn foo<R, F>(f: F)
where
    R: Trait,
    F: 'static + Fn(&str) -> R,
{
    let local = String::new();
    f(&local);
}

fn main() {
    // This is the only part that doesn't work
    foo(std::trim);
}

R is a single, concrete type; for 'static to apply, R must be 'static. So it can't be a borrow of the input (unless the input is also 'static). However, returning a borrow can work if F is higher ranked, but the bounds would be different:

// Using `unboxed_closures` syntax so we can omit `R` as a type parameter
// (On stable you would use a helper trait)
fn foo<F>(f: F)
where
    F: 'static + for<'any> Fn<(&'any str,)>,
    for <'any> <F as FnOnce<(&'any str,)>>::Output: Trait,

The return type here can depend on the input type, and with str::trim it does. And thus it is not a concrete type over the whole of this implementation -- the type varies per input lifetime -- so it can't be a type parameter, as those always resolve to concrete types. Or in Ariel's words,

after all, what would that T be? for<'a> &'a Foo ? That is not a type

For the case where the parameter in question is an input, this means that impl Trait for fn(T) applies to fn(&'a U) for any concrete lifetime 'a -- any concrete type &'a U -- but it doesn't apply to for<'a> fn(&'a T), because the input is not a single concrete type.

(Recent thread involving trait bounds like these.)

Here comes another paragraph of my haven't-bothered-to-properly-cite recollection: The latter type is a subtype of the former type, and can be coerced to it, and this was good enough for awhile -- it was never the case that impl<T> Fn(T) applied to higher ranked types; instead, coercion would kick in elsewhere. Then the 2017 issue changed things and coercion starting failing to be "good enough"; the work-around was to have both implementations. Then NLL came along and tried to make the two implementations overlapping again, but the fallout was too great. Then came the leak coherence lint to gradually make them illegal, but with continued push-back for certain cases. Then the MCP to recognize these cases as distinct and allow them came along, but hasn't been rolled forward into an RFC or whatever yet. And here we are.


However, this seems to contradict the warning (and also my intuition)

It does, and you're not alone; e.g. this recent issue based around the lint making the things it warns about illegal, contrary to the MCP. And my comments about things changing directions and not being certain. It's a pretty un-great situation; there should be an RFC or fiat or something so that a decision on the direction is made. Or at least the messaging of the lint updated so people stop taking it as a given that it will become illegal, in the meantime.


I do wonder about another comment of Ariel's,

NB: With higher-ranked types you could theoretically have something like impl<A<*>, B<*>> JSTraceable for for<'a> fn(A<'a>) -> B<'a> , but that opens it own can of worms.

as I have pondered the possibility of such a type-constructing generic parameter myself when these higher-ranked bound discussions come up. But I'm not a type theorist or compiler dev and have no idea as to the worms. They can't be implicit though; that would make it so you couldn't collect a bunch of T into a (non-'static) Vec as it may be inferred to be a T<*>, say.

This whole topic also reminds me of variadic generics (different types based on some vector of lifetimes you're generic over). There is also occasionally mention of some desired ability to implement traits for all function pointers (and maybe Fn-like implementers? Unclear).

But these alternative approaches seem quite distant indeed, compared to an active MCP.

The fact that (as far as I know) Rust is reserving the right to let subtyping kick in "anywhere" as opposed to more formal coercion sites does make it more treacherous to allow implementations on both super- and sub-types. Which in turn reminds me of some of the conversation here, trying to suss out if subtyping actually is anything more than coercion with variance. (And also more examples of people taking the lint to mean the issue is settled in that thread.)