Generic function failing to satisfy "any" lifetime on argument

I do not understand why the following code fails:

fn call<F>(f: F)
where
    F: FnOnce(&u8),
{
    f(&0)
}

fn foo<T>(_: T) {}

fn main() {
    call(foo);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error: implementation of `FnOnce` is not general enough
  --> src/main.rs:13:5
   |
13 |     call(foo);
   |     ^^^^ implementation of `FnOnce` is not general enough
   |
   = note: `fn(&'2 u8) {foo::<&'2 u8>}` must implement `FnOnce<(&'1 u8,)>`, for any lifetime `'1`...
   = note: ...but it actually implements `FnOnce<(&'2 u8,)>`, for some specific lifetime `'2`

error: could not compile `playground` due to previous error

The compiler claims that foo only takes &'_ u8 for a specific lifetime, but surely that is false? foo is generic over any input type and shouldn't that include references with any lifetime? Is this a compiler bug?

Is there a reason why this should not work?
How can one pass a generic function to something like call?

Thanks for your help!

Beta abstraction makes this compile (i.e., call(|x| foo(x))) but that seems unsatisfactory.

2 Likes

Here:

fn foo<T>(_: T) {}

T must monomorphize down to a single concrete type. Only &'some_specific_lifetime u8 can monomorphize down to T; &u8 with two distinct lifetimes are two distinct types. So you can't use foo::<&u8> to mean "&u8 with any lifetime", for example; a single lifetime will be inferred.

For a more explicit version of the closure wrapping workaround, consider this:

fn foo<T>(_: T) {}
fn bar<T>(t: &T) { foo(t) }

fn main() {
    call(bar);
}

You can have bar::<u8> which still takes a &u8 with any lifetime.

The short answer is that there are soundness issues with doing otherwise (which I realize is a completely nonsatisfying answer without any citations, sorry.)

7 Likes

Thanks for the replies and workarounds!

So foo<T>(_: T) is in that respect less general by design than foo<'a>(_: &'a SomeType) since the latter is able to be "monomorphized" (probably not the correct term for lifetimes?) down to the family of types for<'a> fn(_: &'a SomeType)?

That seems like an unfortunate distinction, it would be nice for <T>(_: T) to be a superset of all more specific types including <'a>(_: &'a SomeType).

Anyways, given that distinction, I guess the error message makes some kind of sense from the compiler's point of view. As an aside, the compiler messages for more complex examples (closer to what I started with) are even more confusing with it complaining about lifetime mismatch but in some cases giving the same type for "expected" vs "found".

fn call<F>(f: F)
where
    for<'r> F: FnOnce(&u8, &'r u8),
{
    f(&0, &0)
}

fn foo<T>(_: T, _: &u8) {}

fn main() {
    call(foo);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0308]: mismatched types
  --> src/main.rs:11:5
   |
11 |     call(foo);
   |     ^^^^ lifetime mismatch
   |
   = note: expected type `for<'r> <for<'r> fn(&u8, &'r u8) {foo::<&u8>} as FnOnce<(&u8, &'r u8)>>`
              found type `for<'r> <for<'r> fn(&u8, &'r u8) {foo::<&u8>} as FnOnce<(&u8, &'r u8)>>`
note: the lifetime requirement is introduced here
  --> src/main.rs:3:16
   |
3  |     for<'r> F: FnOnce(&u8, &'r u8),
   |                ^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `playground` due to previous error

The former is less general along some axis because it can't monomorphize into a higher-ranked type, or satisfy a higher-ranked trait bound. [1]

I feel I should flesh out what higher-ranked means. It mean generic over at least one lifetime -- the for<'a> annotation, which is sometimes implicit. Some higher-ranked types are:

  • for<'any> fn(&'any u8)
    • aka fn(&u8)
  • dyn for<'any> Fn(&'any u8) (usually seen within a Box)
    • aka dyn Fn(&u8)

Each one is a single type; a higher-ranked type is a subtype of every version obtained by replacing higher-ranked lifetimes with concrete lifetimes. [2] So it is the subtype of some family of types, but also a concrete type on its own.

A higher-ranked trait bound (HRTB) that those types can satisfy is:

  • for<'any> FnOnce(&'any u8)
    • aka FnOnce(&u8)

And more generally other traits can be higher-ranked too

  • dyn for<'any> Trait<'any> type
  • for<'any> Trait<'any> bound

But non-Fn-like traits (as objects or bounds) don't have the lifetime-eliding sugar.


foo<T>(_: T) can coerce to any fn(&'a T) [3]. It is a superset in that sense; that's also why you can wrap up foo in something else and have it compile. However, foo itself

  • cannot coerce to all possible fn(&'a T) at the same time, i.e.:
  • cannot coerce to for<'any> fn(&'any T), i.e. after applying syntactic sugar:
  • cannot coerce to fn(&T).

Quoting this comment regarding higher-ranked types like fn(&T),

One problem with these types, is that they can't directly be decomposed into smaller types. for<'a> fn(&'a Foo) -> Bar is not equal to fn(T) -> Bar for any T (after all, what would that T be? for<'a> &'a Foo ? That is not a type, and would give us fn(for<'a> &'a Foo) -> Bar , which has the for in the wrong place).

You would need some sort of type-constructing generic parameter (spelled T<*> in that comment), or higher-kinded types more generally, or at least higher-ranked reference types -- but Rust has none of these. Even if fn(T) is considered a subtype of fn(&T) on some level, they can't coerce as the language is today.


So much for types, but you were asking about a bound. The practical advice, I'm afraid, is something like "yeah sorry... work around it." The rest of this section meanders on about the background reasons why and the like, and where the language might be going, which probably don't mean much to you on a practical level.

As for for the higher-ranked trait bound, ...

...the difference of the two fn signatures can be reasoned about using the implementations here:

struct Foo<F>(F);
struct Bar<B>(B);

trait Trait<T> {}

impl<'a, T> Trait<&'a T> for Foo::<&'a u8> {} // akin to foo<T>(_: T)
impl<'a, T> Trait<&'a T> for Bar<T> {}        // akin to bar<T>(_: &T)

fn quz<U: for<'any> Trait<&'any u8>>() {}

fn main() {
    // OK
    quz::<Bar::<_>>();
    // error: implementation of `Trait` is not general enough
    quz::<Foo<_>>();
}

You need the lifetime to be constrained by the trait but not the type, but with foo::<T> the lifetime must be part of T.

One could still argue that foo<T>(_: T) and similar should still somehow satisfy any Fn(&U) bound; or more generally, that impl<T> Trait for fn(T) should cover the higher-ranked fn(&U) as well. But that comes with it's own challenges and limitations. It was, however, the way that the language seemed to be headed in 2018. The change was backed out "temporarily" as it made these two implementations overlap, for example:

impl<T> Trait for fn(T) {}
impl<T> Trait for fn(&T) {}

Because they became overlapping, they were rejected, but previously they were accepted. (There were other newly rejected patterns that were actually unsound, though.) Since then, direction changed again,[4] and now that particular example is on track to be explicitly allowed (again). So it seems unlikely to become possible in the general case. The door may still be open for a special case around things fully encapsulated by the compiler (via magic), e.g. the implementation of Fn-like traits by function pointers and perhaps other unnameable types like function items and closures. [5]

But I wouldn't hold my breath.

As is probably clear, it's an area of active development -- within the bounds of acceptable breakage.


Yeah, #29061 I think, or some number of other reports. I guess the lack of syntax for "some explicit but non-higher-ranked lifetime" makes it hard to be succinct.

I'm guessing you got there from here. The fact that the hint only un-sugared the second higher-ranked argument is a clue, but not one I'd actually expect anyone to pick up on. Similarly if you had been more explicit.


  1. On the other hand, the latter can only take references, while the former can take non-references. ↩︎

  2. There are some nuances around bounds between multiple lifeitmes and the like which I will ignore here. ↩︎

  3. i.e. any specific 'a ↩︎

  4. read more than you ever want to about it here ↩︎

  5. Though there is the philosophy of language design argument that the compiler should strive to limit implementing things in ways impossible for users of the language. ↩︎

4 Likes

I really appreciate the detailed reply! That makes sense. And thanks for humouring my "why doesn't rust just do this coercion automatically, safely inferring relationships between higher kinded types can't be that hard" (/s) frustration.

Nice to see there was push to support this but yeah I guess I should have expected that it would cause problems.

Correct. I noticed it was only adding the explicit lifetimes for the second argument but I didn't know what that meant, or for that matter understand at all how what it was asking for differed from what I was supplying. So I tried making the bound exactly the same as it suggested. At least I've now learned that Fn(&T) is syntactic sugar for for<'a> Fn(&'a T), I wasn't sure if they expressed the same bound or not.

1 Like

Intuitively, I would've expected such a coercion to work. My naive thought process would be:

  • For a type to be coercible to a for<'a> fn(&'a u8), it must be coercible to fn(&'a u8) for all lifetimes 'a. This condition is necessary and sufficient.
  • A fn<T>(T) function item is coercible to fn(&'a u8) for all lifetimes 'a, via monomorphization.
  • Therefore, it should be coercible to a for<'a> fn(&'a u8).

As you've explained, the problem is in the first premise. However, it goes against my most immediate understanding of what a for<'a> bound means. My question is, if this is not the necessary and sufficient condition, then what is? (I can't quite follow the esoteric discussions of leak-checking vs. universes in the issues you've linked.)


This reminds me of a recent issue I had helping another user on the community Discord server. They wanted to store a type-erased list of async callbacks that took a reference as a parameter. Since I couldn't express the bounds directly (for lack of being able to write trait bounds on a Fn's output type), I tried to write a helper trait:

use futures::future::BoxFuture;
use std::future::Future;

pub trait Callback<'a> {
    fn call_boxed(&self, slice: &'a [u8]) -> BoxFuture<'a, String>;
}

impl<'a, F, Fut> Callback<'a> for F
where
    F: Fn(&'a [u8]) -> Fut,
    Fut: Future<Output = String> + Send + 'a,
{
    fn call_boxed(&self, slice: &'a [u8]) -> BoxFuture<'a, String> {
        Box::pin(self(slice))
    }
}

pub struct CallbackList {
    callbacks: Vec<Box<dyn for<'a> Callback<'a>>>,
}

impl CallbackList {
    pub fn new() -> CallbackList {
        CallbackList {
            callbacks: Vec::new(),
        }
    }

    pub fn push<F>(&mut self, f: F)
    where
        F: 'static + for<'a> Callback<'a>,
    {
        self.callbacks.push(Box::new(f));
    }
}

This compiles without issue, and it seems like it should work. However, it turns out that it doesn't accept closures (Rust Playground):

fn main() {
    let mut list = CallbackList::new();
    list.push(|slice| async { format!("{slice:?}") });
}
error: implementation of `Callback` is not general enough
  --> src/main.rs:39:10
   |
39 |     list.push(|slice| async { format!("{slice:?}") });
   |          ^^^^ implementation of `Callback` is not general enough
   |
   = note: `[closure@src/main.rs:39:15: 39:53]` must implement `Callback<'0>`, for any lifetime `'0`...
   = note: ...but it actually implements `Callback<'1>`, for some specific lifetime `'1`

error: could not compile `playground` due to previous error

Is this related to the issue of fn_item as fn(&u8), or is it another issue entirely limited to closures?

1 Like

Well, function items are pretty special. Let's start with something that has more visible language surface area, dyn Trait. Roughly what it comes down to is that in order to coerce to a dyn Trait, you need to have an impl Trait somewhere (either explicit or notionally, supplied by the compiler).

If you want a higher-ranked dyn Trait, the implementation itself is going to have to be generic over some lifetimes (which could be part of types) in relation to the type implementing the trait. So here:

struct StandAlone;
struct Contains<T>(*const T);

trait Trait<T> {}
impl<T> Trait<T> for StandAlone {}
impl<T> Trait<T> for Contains<T> {};

The implementation for Contains<&'static u8>, say, is an implementation of Trait<&'static u8> -- every concrete type has a distinct implementation, because the T must unify between the trait and the implementing type. Whereas for Standalone, there is an implementation for T -- if we had such a concept in rust, for<T>. At any rate, the same implementation applies to every &u8 -- that is, there is a single implementation for all &u8 -- for<'a> &'a u8. Thus, this compiles:

    let _: &dyn for<'x> Trait<&'x u8> = &StandAlone;

But this does not:

    let _: &dyn for<'x> Trait<&'x u8> = &Contains::<_ /* ?? */>(0 as _);

Now let's consider function items, but instead of coercion to function pointers, let's stick with coercion to Fn-like traits. There needs to be a notional implementation of the trait. Let's consider a couple of implementations:

fn foo<T>(_: T) {}
fn bar<T: ?Sized>(_: &T) {}

The implementations are roughly:

impl<T> Fn(T) for foo::<T> { /* ... */ }
impl<'x, T: ?Sized> Fn(&'x T) for bar::<T> { /* ... */ }
// aka impl<T: ?Sized> Fn(&'_ T) for bar::<T> { /* ... */ }
// aka impl<T: ?Sized> Fn(&T) for bar::<T> { /* ... */ }

In both cases, the T must unify between the trait and the implementing type. But in the implementation for bar, the lifetime is independent of the implementing type. Therefore this works:

    let _: &dyn Fn(&u8) = &bar<u8>;

But this doesn't:

    let _: &dyn Fn(&u8) = &foo::<_ /* ?? */>;

Can we influence these notional trait implementations? To some extent, yes. If you put a bound (even if trivial) on a lifetime, it will become a parmeter of the function item:

fn bar<'a: 'a, T: ?Sized>(_: &'a T) {}
// ...
// This now fails like `foo` does
    let _: &dyn Fn(&u8) = &bar::<u8>;

What's going on? Well, if the bound wasn't so trivial, it might not be representable by the relatively coarse for<'any> higher-ranked types and bounds we have today. [1] It's part of a system to preserve soundness.

In compiler-implementation technical terms, this is a distinction between a function's parameters being late-bound or early-bound. Sometimes this arguably-an-implementation-detail surfaces in relatively obscure errors. Like the link goes into, a parameter is late-bound if it's a parameter of the trait in an implementation, but not parameter of the type. If it's a parameter of the type, it's early-bound. [2]

My mnemonic is "late-bound is trait-bound is higher-rank sound (is not turbo-fishable)."

So we have one potential answer to your question: A lifetime parameter must be late-bound in order to coerce into a higher-ranked lifetime.

Alright, let's consider this OP again:

fn foo<T>(_: T) {}

T here is certainly a parameter of the function item -- we can turbofish foo::<u8>, say. Can we go the other direction and make T late-bound, in some reverse of how we made 'a early-bound? Not today; type parameters are always early-bound. The impl Trait initiative document I linked above, though, suggest an alternative -- impl Trait in argument position could be late-bound. That is,

fn foo(_: impl Sized) {}

could have a notional implementation

impl<T /* : Sized */> Fn(T) for foo /* no parameters */ { /* ... */ }

And if that happens, presumably you could coerce it to a higher-ranked dyn type, ala Standalone above. One step beyond that, perhaps a witness of such a dyn Fn implementation for a function item would permit coercion to the analogous function pointer.


I haven't talked about higher-ranked function pointer types yet. I also haven't played with it sufficiently to be completely confident in this section, and I'm running out of steam to run down references, so this is going to be pretty terse.

Function pointers don't have generic parameters, and (like dyn Trait) can only be higher-ranked over lifetimes, not types. So you might think that you would need to start with a

for<'a_1, 'a_2, 'a_3, ..., 'a_n> $X

in order to coerce to a

for<'b_1, 'b_2, ..., 'b_m> $Y // m <= n

With some suitable replacement between $X and $Y as well. And I think this used to be the case. However, the compiler's concept of how subtyping works has evolved, and now it can prove not only that

// This first one is aka fn(&u8, &u8) */
for<'a, 'b> fn(&'a u8, &'b u8) is a subtype of
for<'c    > fn(&'c u8, &'c u8)
// By letting 'a => 'c and 'b => 'c

But also that

for<'c    > fn(&'c u8, &'c u8) is a subtype of
for<'a, 'b> fn(&'a u8, &'b u8)
// As you can use variance to unify to the lesser of `'a` and `'b`

And thus, they are the "same" type, and can coerce into each other.

There's some PRs or issues or blogs where Niko talks about this, but unfortunately I couldn't find them offhand.

What are the exact rules? I'm not sure. Does this equality hold everywhere? PR 72493 got us closer, but still no. Are the types actually normalized? Also no.

I think this issue implies it's not even in the dev guide proper.


I agree it can all be quite confusing, since you can end up with one type which is a subtype of another in some sense -- including some iterations of rustc's implementation as I understand it -- yet you cannot coerce between them (for the sake of language semantics, or coherence, et cetera).


I believe the problem in that playground is just one of higher-ranked inference, ala RFC 3216. Your implementations are for a concrete lifetime, so it's okay that the return type must be a concrete type -- it can still capture the lifetime; additionally, the implementing type doesn't have the return type (Fut) as an input (parameter), but instead as an output (an associated type of the Fn-like traits).

That said, async is not my forte, so I might be missing some deeper difference between the closure and the function. I didn't manage to find a way to coerce the closure. I think it's a known problem, but again I'm not an async expert. Maybe those macros can work around it; I didn't try.


However, if you needed to bind the return type under a higher-ranked bound like so:

where
   // This stable syntax is already problematic as you can't elide `-> Fut`
   F: /* for<'_> */ Fn(&[u8]) -> Fut,
   // But even on unstable, you have a problem if you need this
   Fut: SomeAbility,

Then that would be a problem if you want different input lifetimes to result in different output types, as Fut is a single type due to being a type parameter. So that situation is related.

I have played with this situation for Fn(&'a) -> R bounds quite a bit. I'll just refer to this earlier post (the rest of that thread covers similar ground as the recent parts of this one); it links to this thread which covers a solution (up to a point) for that case on stable. [3]


  1. Or maybe it would be representable, but only by dint of implied bounds elsewhere. If you're tracking the discussion around the bad ergomics between HRTB and GATs, it's pretty related; there are a lot of rough edges in complicated combinations. Someday maybe we'll have for<'a where 'b: 'a> style higher-ranked types. ↩︎

  2. Lifetimes are early-bound if they have bounds, or if they only appear in the return type. If they're unbound and in an input parameter, they're late-bound. "If they're explicit they're early-bound" would be more intuitive, but inadequate for unifying two input lifetimes, say, not to mention backwards incompatible. ↩︎

  3. I have most of a writeup about a refinement to that particular use case (though not handy on this computer) -- it is one of a few extremely illustrative use-cases that have motivated a "lifetime reference" I've wish I could sink time into for months now. As this very reply demonstrates though, it's a very complicated and "why is the sky blue, but why, but why..." sort of topic, and knowledge must be both accumulated and adjusted as the compiler changes, due in no small part to the lack of proper documentation. ↩︎

2 Likes

Thanks for the explanation! I'd heard of early- vs. late-bound parameters before, but they make much more sense with your impl<T> Trait<T> for Type vs. impl<T> Trait<T> for Type<T> analogy. It also matches with a hunch I had, that the issue was related to the absence of for<T> types: if they existed, the type parameter could be late-bound (i.e., as impl<T> Fn(T) for foo), and we could write foo <: for<T> fn(T) <: for<'a, T: ?Sized> fn(&'a T) <: for<'a> fn(&'a u8).

I'm curious about the proposal that argument-position impl Trait could be used to enable this. It seems to require that such late-bound type parameters are allowed in function items, where they currently aren't allowed today. To me, it seems very odd that the two syntaxes (APIT vs. turbofish) should have such different behavior. If function items could have late-bound types, it would be nice if they could also have a "magic turbofish" that can resolve a type argument into an HKT (e.g. to resolve foo::<&'? a> into for<'a> fn(&'a u8), where '? is some kind of "magic" syntax).

That explicit bounds make lifetimes early-bound also explains another issue I was having helping another user. They had a function that accepted an impl Fn(&mut Type<'_>), but they couldn't get it to accept a fn foo<'a, 'b: 'a>(_: &'a mut Type<'b>). It worked after I suggested removing the explicit 'b: 'a bound. At the time, I chalked it up to implied lifetime bounds being weird as always, but now I see how the lifetime it was early-bound, even though it is identical to the late-bound implied relationship.

I've definitely seen the powerful subtyping for higher-ranked function pointers before, as part of arielb1's explanation of the ancient implied-bound soundness hole:

    // ['c comes from the environment]
    let foo2: for<'a> fn() -> fn(Option<&'a &'c ()>, &'c T) -> &'a T = foo1;
    // subtyping: contravariantly 'c becomes 'static
    let foo3: for<'a> fn() -> fn(Option<&'a &'static ()>, &'c T) -> &'a T = foo2;

(This is unsound, since we quietly dropped the implied 'c: 'a bound.) While trying to understand this, I came to the idea that one function-pointer type is a subtype of another iff all valid instantiations of the latter with specific lifetimes are also valid instantiations of the former. But this only applies to late-bound lifetimes, not our early-bound foo from above.


Overall, I wish this wasn't all so esoteric. There's good soundness reasons for our current status quo, but it keeps biting intermediate Rust users who just want to handle closures that take references. At the very least, the error messages could be much more helpful than they are today.

Discussed here and in particular this comment, although I'm not sure it's the "original" I was thinking of.

However!

Yeah, turns out this is a problem.

I'm not 100% on everything about that proposal either (and don't like that it makes APIT[1] so much more important, as I think that was a misstep in the language [2], but realistically that ship has long sailed). I too would want a more general framework to handle various things. We should be thinking of more general HR bounds (for<where> or whatever), and if we have HR-over-types, eventually more complicated bounds for them as well. (One interesting side-node is that APIT inherently consists of at least one explicit bound, the trait.)

For some explicit things the impl Trait sketch falls short at (in comparison with lifetimes today), consider the reasons "explicitly named == early bound" doesn't work:

  • fn hr_over_the_lifetime<T>(one: &T) {}
  • fn hr_over_the_lifetime_still<'a, T>(one: &'a T, two: &'a T) {}
  • fn hr_over_the_type(one: impl Eq) {} // proposed
  • fn nope_sorry<T: Eq>(one: T, two: T) {}

<T: impl Eq> maybe? Well, it'll be a contentious bike shed to paint regardless I imagine; some people really seem to hate <>.

Yeah, that's the "[nuance] around bounds between multiple lifeitmes and the like" I was thinking of. I find it interesting that the direction seems to be going towards for<where> instead of more like this way (quoting LHolten):

To me it looks like function declarations assume all instances of the same lifetime in the signature to be identical.
But when sub-typing functions, you don't have to substitute all instances of a generic lifetime at the same time (thus breaking the assumption).
Meanwhile for functions generic over types (instead of lifetimes), all instances of the type have to be substituted at the same time.
So why not apply the same restriction to substituting generic function lifetimes?

(N.B. the lifetimes need not be generic.)

I had the same question, but it didn't really click until just now that it would break the subtyping/type-equiv rules between

fn f<'a    >(&'a str, &'a str);
fn g<'b, 'c>(&'b str, &'c str);

at least, if trivially applied. So it would need to be a bound-aware rule, probably. I haven't given it much re-thought, but for<where> is likely the more robust approach anyway.

For those following along, #25860 is the issue, and I like lcnr's dive on the topic, which illustrates contravariance is not necessary for unsoundness / killing contravariance is not sufficient to restore soundness.

Skimming that article again, they also have a section on early vs. late bound -- making the implicit bound explicit makes the lifetimes both early-bound, which makes them non-higher-ranked, which "solves" the problem (but breaks other things by losing expressiveness). This can be taken as a concrete example of (some of [3]) the vague "for soundness" hand-waving I was doing -- implicit bounds allows a way to get a higher-ranked type out of a lifetime with bounds, and the result is (currently) unsoundness.

Tangentially, I'd love to see someone knowlegable write up all the HRTB and GAT related issues in this context as well, where adding or removing explicit bounds for things that have implicit bounds changes what implementations are considered valid, which ones suddenly require holding inference's hand, etc. It really seems the different parts of the compiler either assuming the implied bounds, enforcing them, or ignoring them are not coming together into a cohesive (or sound) whole.


  1. argument position impl Trait ↩︎

  2. in the stabilized form anyway ↩︎

  3. ↩︎

Out of curiosity, how exactly would for<where> help with the soundness hole and related issues? By itself, it would seem to be exactly as powerful as the existing implied bounds in for<> (with some extra dummy parameters). Obviously, it can provide some much-needed clarity to HKTBs and late-bound lifetimes from the user's perspective, but at best it seems to better illuminate these issues, rather than solving them in its own right.

lcnr covers for<...> where<...> aka for<... where ...> as pertains to #25860; basically in that context it means

  • You start with something well-formed, that may have implicit bounds
  • You carry those around as for<where> bounds
    • Maybe explicitly, maybe just internally
  • When you replace (and/or split) lifetime parameters, they hold on to their for<where> bounds
    • Even if in replaced form, they are no longer implied by the rest of the signature

That last part is the key difference as you can end up with something like a

  • for<'out where 'in: 'out> fn(&'out &'static (), &'in T) -> &'out T

Which is higher-ranked but has bounds beyond those implied by well-formedness. In the example, the bound came from originally having a &'out &'in () as the first parameter, and you return the second parameter. Without the bound, you could from here change 'out to 'static and extend the lifetime of the second parameter unsoundly. With the bound, making that change forces 'in to be 'static as well.

I think we'll want an explicit form anyway though, because sometimes higher-ranked but-bound lifetimes solve other problems, and the only way we have to get them now is by exploiting implied bounds in hacky ways. Such a workaround is part of the pending scoped threads implementation.

There's an RFC PR but I'm not sure it's thorough enough in its current form to get traction. The comment is a bit disheartening; I'm of the view that as much things as possible which are implicit should have an explicit form (preferably with a 1-to-1 desugaring). [1]

On the up (?) side, I don't think a purely implicit form can actually fix things like #25860 without full-program analysis/data-flow (or some rather artificial restrictions around not passing stuff with for<where> bounds between functions or the like).


  1. The inscrutable errors, undocumented algorithms, and obscure hints and tricks used to hack around both of them highlighted in this very thread being a prime example of why. ↩︎

2 Likes

:100: I. Wholeheartedly. Agree. :100:

  • I can't see how something like Scope<'scope, 'env>, with an implicit bound which won't even be rendered in rustdoc, could be better than the way clearer
    for<'scope where 'env : 'scope> … Scope<'scope>:

    • On the left: good luck figuring out the role of 'env without diving into the internals of the implementation of scoped threads!

    • On the right: "ah, ok, 'env is just an upper-bound on the universal for<'scope> quantification! It doesn't play any other role whatsoever on the Scope type" (since 'env no longer appears there!)

1 Like