I poked at it enough to get it working. Do you control all these traits and types by the way?
Some things I noted upfront:
-
You have elided lifetimes in ALLFUNC
. Here's the expanded definition:
#![deny(elided_lifetimes_in_paths)] // helps to find these types of errors
const ALLFUNC: [SomeDatFunc<'static>; 2] = [
SomeData::foo,
SomeData::bar,
];
-
Maybe you meant to have a higher-ranked type here instead...
pub type SomeDatFunc = for<'a> fn(&'a str, &'a str) -> Option<SomeData<'a>>;
...in which case you need an adjustment elsewhere:
-pub fn foo<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'a>>
-where
- 'x: 'a,
-{
+pub fn foo<'x>(first: &'x str, second: &'x str) -> Option<SomeData<'x>> {
(And similarly for bar
.) The reason are those HRTB limitations I mentioned. Note that being more general with the lifetimes didn't help you from a practical point of view: callers can coerce to the shorter lifetime at the call site with the new signature -- and they were doing so already in practice, because no one wants to borrow something longer than required.
-
This implementation can be more general:
-impl<'a> TraitSomeData<'a> for SomeData<'a> {
- fn whatever(&'a self) -> &'a str {
+impl<'a> TraitSomeData<'_> for SomeData<'a> {
+ fn whatever(&self) -> &'a str {
self.data
}
}
You're just copying the shared reference self.data
out. You don't need to force yourself to be remain borrowed for 'a
. This was also a required change for the fix, and it's also part of why I asked if you controlled all the traits. It feels like there may be some mismatch between the traits and what is actually being implemented. That said, I stopped trying to figure out the overall design once I got things to work.
Before this change you were taking a self: &'a SomeData<'a>
, which is sometimes okay since we're dealing with shared references here, but it's still a yellow flag. It was problematic in this case.
With those out of the way, we can address the topic at hand. The problem is that the Fn
trait sugar has become salt by forcing you to name the (potentially borrowing) output type. But when you're being generic, the only way you have to name the output is a generic type variable -- which can't represent more than one single type.
For the playground, it's sufficient to use a subtrait to avoid having to name the return type.
pub trait Call<'x>: Fn(&'x str, &'x str) -> Option<Self::Out> {
type Out;
}
impl<'x, F: Fn(&'x str, &'x str) -> Option<Out>, Out> Call<'x> for F {
type Out = Out;
}
Note how we parameterized the trait on the lifetime -- so here the Out
can still be borrowing (be a type that mentions 'x
), because we're only dealing with one lifetime, not all lifetimes yet.
With this trait, your bounds will look like this:
- fn doit<F, X>(f: F, first: &'a str, second: &'a str) -> Option<X>
- where
- for<'x> F: Fn(&'x str, &'x str) -> Option<X>,
- for<'x> X: TraitSomeData<'x>;
+ fn doit<F>(f: F, first: &'a str, second: &'a str) -> Option<<F as Call<'a>>::Out>
+ where
+ for<'x> F: Call<'x, Out: TraitSomeData<'x>>;
So you'll have to be able to change the bounds on the traits. If you want to make them simpler, you could move the Out: TraitSomeData<'x>
bound to the Call
trait's associated type.
With all the changes above, the playground compiles.