I don’t think there is a way. After all, lifetimes on references are the main reason that HRTB even exist. The fact that an example like yours does not work without them is, as far as I’m aware, the reason we have HRTB.
The problem is that you cannot name the lifetime that you need the trait impl for. It is (well, at least it can be) the lifetime of the body of the is_even function, if you will. Since that lifetime does not have a name, the solution is a higher rank trait bound that just requires the trait impl for all lifetimes.
Also, in a world where HRTBs do not exist yet, one might think that the more obvious solution would be to add a feature for naming “the body of is_even” as a lifetime, but I suppose that is suboptimal in that lifetimes are not necessarily lexical and the borrow checker is allowed to do quite some analysis to find out if programs are “safe”.
I think you have it backwards in that the latter is more general than the former. Presumably, the meaning of the for<'a, 'b> version is that the function can operate on two references with different lifetimes, while the for<'a> version requires identical lifetimes. But when you call the function, the compiler will automatically narrow the lifetimes to the smallest set that satisfies the interface (exactly the overlap of 'a and 'b), so in practice they are equivalent. You only require the borrows to be valid while you're calling rem, as you're not returning anything referencing the inputs, so there's no need to make things more complicated.
Depending on what you are talking about. We’re talking an impl<T> … with a constraint on T that T it needs to satisfy in order to get this impl for Even. From this standpoint the latter is a harder constraint than the former, so that in effect, the former trait impl is more general.
I don’t see how/why the compiler would want to narrow down 'a and 'b to be the same if it istn’t asked to do so. Sure it will do so if we write
for<'a> &'a T: std::ops::Rem<&'a T, Output=T>
and it sees some impl like
impl Rem<&i32> for &i32
(i.e. impl Rem<&'_ i32> for &'_ i32, i.e. impl<'a,'b> Rem<&'a i32> for &'b i32)
that it has to use.
For some practical takeaway here: If someone implemented a CustomNumber type and accidentally only provided an
impl<'a> Rem<&'a MyNumber> for &'a MyNumber
(I mean, it does look similar to impl Rem<&MyNumber> for &MyNumber) then if we also required the
for<'a, 'b> &'a T: std::ops::Rem<&'b T, Output=T>
constraint without really needing it, we’d have a problem.
Yes, you are right, I was too focused on the lifetimes and didn't consider this possibility.
That was exactly my thought. It seemed too restrictive to ask in the constraint the implementation for all lifetimes, instead of only the one I need. I expected some way to express something like this:
impl<T> IsEven for T
T: Zero + One + PartialEq,
fn is_even<'a>(&'a self) -> bool
&'a T: std::ops::Rem<T, Output = T>,
self % (Self::one() + Self::one()) == Self::zero()