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>, {}