Do any workarounds exist for the following borrow checker issue?

I ran into this problem and I'm quite sure the is not a mistake on my end but a shortcoming in the borrow checker.

type Key = (); // todo! will become a kind of keyboard input result, like a char
type Action<T> = for <'a> fn(<T as Scene>::ActionArg<'a>) -> <T as Scene>::ActionRes;

trait Scene
{
    type CursorPosition;
    type ActionArg<'this>;
    type ActionRes;
    fn getPossibleActions(cursor : Self::CursorPosition) -> &'static [(Key, Action<Self>)];
}

This does not compile because of getPossibleActions. To me, it seems like a perfectly reasonable thing to ask without restricting Self to 'static, which the compiler is now asking me to do. In fact, I am certain that Self is not restricted to 'static.

Is there a way to fix this without giving CursorPosition a generic associated lifetime?

If I do

impl<'s> Scene for &'s MyType {
    type ActionArg<'this> = &'s String;
    type ActionRes = &'a str;
    // ...
}

Then

for <'a> fn(<T as Scene>::ActionArg<'a>) -> <T as Scene>::ActionRes

Is effectively a

fn(&'s String) -> &'s str

So at least those must be restricted to not have lifetimes that aren't part of the GAT inputs. (If Self is 'static, there are no such lifetime possible to name.)


That said, adding such bounds doesn't get rid of the error, so there may be room for improvement in the language here.

(Or there may be some reason that Self/T can't always be normalized out of the <T as Scene>::... projection and that's a problem. I'm not complete sure either way.)

Self is a type with one specific lifetime. There is no syntax to get the kind[1] that is 'static. You can add another entry type This: Scene + 'static; and have it return this instead.


  1. variant but likely illegal if by rules ↩︎

I see. Do you think this is worth reporting an issue/feature request for or is this already being worked on behind the scenes?

it's easy to forget that an associated type (not just GAT) can depend on the trait input parameters in addition to the direct parameters of the GAT itself, and Self can be seen as an (implicit) input type to a trait, even if the trait has no other generic parameters.

for example, from the perspective of the type checker, <T as Trait<U>>::Gat<X> can be seen as if it were some generic type Gat<T, U, X>.

in your example, Action<Self> can potentially depends on Self in two places:

  • the function return type <Self as Scene>::ActionRes

    • this type could depend on Self, however, a function pointer does NOT require its return value be 'static for the function pointer itself be `'static', so this is not the problem
  • the function argument type <Self as Scene>::ActionArg<'a>

    • this type can also depend on Self, let's take a simple example, say ActionArg<'a> = &'a Self, this requires Self: 'a.
    • another example that depends on Self can be ActionArg<'a> = Self, similar to what @quinedot has shown.
    • and when you use a "forall" lifetime 'a, 'a can be subsituted with 'static for type checking, that's why you need Self: 'static

in generic context, the type must be checked conservatively [1].


  1. I think implied bounds are treated special for higher ranked lifetime bounds, but I don't know how or if you can use any tricks to pass the type checker in this case ↩︎

1 Like

Since you are asking for a workaround, could this be an option?

type Key = (); // todo! will become a kind of keyboard input result, like a char

trait Action<T: Scene> {
    fn run(&self, arg: T::ActionArg<'_>) -> T::ActionRes;
}

trait Scene
{
    type CursorPosition;
    type ActionArg<'this>;
    type ActionRes;
    fn getPossibleActions(cursor : Self::CursorPosition) -> &'static [(Key, &'static dyn Action<Self>)];
}

You can try (with bounds on the GAT and associated types). It might be a dupe; GATs have a lot of shortcomings around higher-ranked bounds and types. On the other hand, sometimes erasing or forgetting lifetimes can cause soundness issues, even the erased lifetimes are only used to define other types which are 'static. If something like this is preventing soundness issues, accepting your use case might not be possible.

I came back to play around with ways to work around this.

Here's a playground where I've pushed the error to be post-monomorphization -- everything works until you actually try to call the associated function in a generic context, at which point the same error fires. Presumably because it's only trying to prove...

    type Action: 'static + for<'a> Fn(Self::ActionArg<'a>) -> Self::ActionRes;

...once you actually make the call for whatever reason.[1]

Here's another where I've added enough indirection so that you can call things, but now it's not guaranteed that you have function pointers. You can add bounds to ensure that you do, if you need to, but it adds some noise to do so.


I think the differences are basically "who has to prove the HRTBs and do they have enough information to do so (as far as the compiler is concerned)".

  • In the OP, the trait definition has to prove it, but can't; the compiler doesn't doesn't want to allow Self in there if it might not be 'static.

  • In the first playground of this reply, the HRTB on the associated type apparently pushed off the proof until the associated function is called, so the body of test had to prove it... but when the implementing type is generic, it's in the same situation as the trait definition was. When you call it for a specific type, though, the bounds can be proven and it works.

  • In the second playground, the new bounds on test are enough that the compiler is satisfied everything is alright to call the associated function, and now the caller of test has to prove those bounds. If they're generic they'll probably need the same bounds themselves, and if they're calling test with a specific type, it will work (like it did in the first playground).


I also wrote a number of comments in the code which I won't repeat here, but feel free to ask questions.


  1. It works for concrete types. ↩︎

3 Likes

wow, that's quite a lot of type gymnastic. very impressive.

OP didn't provide more context about the use case, but if the ActionArg were not GAT, and Action<T> were fn(&'a ActionArg) instead of fn(ActionArg<'a>), then the problem would not exist in the first place.

many a time, I find myself wanted to use GAT, just to figure out most (or all) of the implementors ended up defining Gat<'a> = &'a SomeType. in my personal experience, GAT in return type is very useful and expressive, but GAT in argument type is usually unnecessary.

a thought experiment: what if rust allowed use<> precise capture in bounds?

// currently NOT valid rust:
trait Scene {
    type ActionArg<'a>: use<'a>;
    type ActionRes: use<Self>;
}
1 Like

That's a good observation. At least two reasons are that it is really for<'a where <T as Scene>::ActionArg: 'a> ..., in that case so it's okay if you put a lifetime from the implementing type in ActionArg. And because 'a definitely can't disappear so, it really is a higher-ranked function pointer, not a fn(SingleType) -> ... in disguise.

Hmm... that is an interesting thought. I think it might be challenging though... I'm thinking of something like

impl<'a, 'b> Scene for &'a MyTy<'b> {
    type ActionArg<'arg> = &'arg str;
}

Now ActionArg<'arg> doesn't use any generics from the implementing type, but there still has to be some way to have different definitions per implementer... you have to "use"(?) the implementing type to distinguish between

<FooBar as Scene>::ActionArg<'static>
<&'a MyTy<'b>>::ActionArg<'static>

at some point in the type system. (There is not one impl Sized + use<'a> being defined for all implementers.)

Also, the latter is only defined when 'b: 'a, as otherwise you have a non-well-formed type. Is it definitely sound to forget about that in all cases? I'm not sure -- near-soundness bugs like these give me pause. The partial fix there was to disallow forgetting that lifetimes were captured -- see "Limitation: Opaque hidden types" here. The latter PR landed, which is great for some borrow-checking scenarios, but we still can't forget captures entirely.

(The discussions do imply there may be a way to make it sound in the future.)

1 Like

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.