Problems matching up lifetimes between various traits and closure parameters

I got rid of most the inference problems, and I believe the ones that remain are just of the typical "rustc failed to realize my closure was higher-ranked" variety -- although the complexity of the situation means the errors aren't so clear about this.

Most of this post is me rambling about the inference fix; feel free to jump to the bottom for links to the working code.


First, a little review.

You wish you could have something like

// (I) -- can't actually work
impl<F, A, R> SignalClosure for F where
    A: FromValue,
    R: ToClosureReturnValue
    F: 'static + Fn(A) -> R,

But this doesn't constrain A (multiple A would work for fn foo<T>(_: T) {}), so you're going to need be generic over at least the input type:

// (II) -- could work for static types
impl<F, A, R> SignalClosure<A> for F where
//                     New ^^^
    A: FromValue,
    R: ToClosureReturnValue
    F: 'static + Fn(A) -> R,

And the input might be borrowed, so A isn't just one type (it varies by lifetime), so you now we need some sort of indirection -- you can't have a type parameter for the argument directly anymore, as that would constrain it to a single type.

// (III) -- works for static return values (but poor inference)

// Alias SIAR.  `Self::Arg=Self` for static types.
trait SomeIndirectArgRepresentation<'a, A> { type Arg: 'a; }

impl<F, A, R> SignalClosure<A> for F where
    A: for<'a> SIAR<'a>,
    for<'a> <A as SIAR<'a>>::Arg: FromValue<'a>,
    R: ToClosureReturnValue
    F: 'static + for<'a> Fn(<A as SIAR<'a>>::Arg) -> R,

And on top of that, the return value might be borrowed, so R isn't just one type (it can vary by lifetime too), so now we need

// (IV) -- works for everything (but poor inference)

// Alias SIAR.  `Self::Arg=Self` for static types.
trait SomeIndirectArgRepresentation<'a, A> { type Arg: 'a; }

impl<F, A, R> SignalClosure<A> for F where
    A: for<'a> SIAR<'a>,
    for<'a> <A as SIAR<'a>>::Arg: FromValue<'a>,
    R: for<'a> SIAR<'a>,
    for<'a> <R as SIAR<'a>>: ToClosureReturnValue
    F: 'static + for<'a> Fn(<A as SIAR<'a>>::Arg) -> <R as SIAR<'a>>::Arg,

And this all works! But rustc can't see through all the indirection. From III onwards it has difficulty with this generic implementation. But it wouldn't have a problem with II at all.

The solution to the inference problems was to use a blanket implementation for SignalClosure<StaticTypes> that looks like II above, and then have a separate implementation for borrowing types like str.

// (V) -- works for everything with better inference

impl<F, A, R> SignalClosure<A> for F
where
    A: 'static + for<'a> FromValue<'a> + SIAR<'a, Arg=A>, 
    // i.e. static types                 ^^^^^^^^^^^^^^^
    R: 'static + ToClosureReturnValue,
    F: 'static + Fn(A) -> R, // Back to using Fn(A), like in II

impl<F> SignalClosure<str> for F
where
    // The bounds are `str` are known, so no input bounds needed
    // But we still need indirection for `R`
    R: for<'a> SIAR<'a>,
    for<'a> <R as SIAR<'a>>: ToClosureReturnValue
    F: 'static + for<'a> Fn(&'a str) -> <R as SIAR<'a>>::Arg,
    // You can still use `SIAR` for the arg here -- you don't need to
    // be explicit that `F` takes `&str` -- because `<str as SIAR<'a>>`
    // is just as unambiguous

This apparently gives rustc enough context to get from the argument types of closures to the corresponding implementation, I think because the bounds on the implementation that is generic over A -- generic over the trait's type parameter -- is now a bound on Fn(A) directly. And if this one doesn't apply, all the other implementations are not generic over A (they cannot be for coherence), and thus they are individually checked; if only one matches, there is no ambiguity.

Note: The actual code has helper traits and some other differences; the above examples are crammed into denser pieces for the sake of discussion.


In summary, the key breakthrough was realizing I could have a blanket simple implementation for static types, but still implement generically-over-closures for multiple borrowing types (so long as I wasn't generic over the type parameter).

Here's the working example for the refactoring I've been working off of, that uses GAT-emulating helper traits. And here's a version closer to your OP, with comments about the additions and changes. I also made some other cosmetic changes, so it's the cleaner one to read. I personally think the GAT-like change to FromValue is nicer than FromValue<'a>, but recognize that may be a pain to change.

To add more types, you would:

  • if static
    impl Indirect<'_> for Ty { type Arg = Ty; }
    
  • if not
    struct StandIn;
    impl<'a> Indirect<'a> for StandIn { type Arg = Ty<'a>; }
    impl <F> SignalHandler<StandIn> for F
    where
        F: 'static + for<'a> ValidOutputter<'a, StandIn>,
    {}
    
4 Likes