Trait whose implementation requires HRTB

Let's say that I want a trait that allows to check whether the value is even or not.
The first definition that comes to my mind would be

use num_traits::identities::{One, Zero};
trait IsEven: Zero + One + PartialEq {
    fn is_even(&self) -> bool;
}

I might want to provide a generic implementation for types implementing std::ops::Rem.

What I cooked up is

impl<T> IsEven for T
where
    T: Zero + One + PartialEq + std::ops::Rem<T, Output=T>,
    for<'a> &'a T: std::ops::Rem<&'a T, Output=T>
{
    fn is_even(&self) -> bool {
        self % &(Self::one() + Self::one()) == Self::zero()
    }
}

which seems to work.

My question is about the line

for<'a> &'a T: std::ops::Rem<&'a T, Output=T>

Should it rather be like this?

for<'a, 'b> &'a T: std::ops::Rem<&'b T, Output=T>

It seems to me that the former is more general than the latter, but I'm probably having troubles understanding correctly these implementations of Rem for reference types.

Is there maybe a way that completely avoids HRTB?

Almost useless observation

I'm also disregarding this other possibility which takes is_even(self) by value because it doesn't work well for types which are not Copy, although the implementation is easier.

IMO it should rather be:

for<'a> &'a T: std::ops::Rem<T, Output=T>`

(playground)

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”.

1 Like

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.

2 Likes

I see what you're saying. My confusion is that I was thinking in terms of the lifetimes for defining the Rem impl, not in terms of using them in a constraint.

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
where
    T: Zero + One + PartialEq,
{
    fn is_even<'a>(&'a self) -> bool
    where
        &'a T: std::ops::Rem<T, Output = T>,
    {
        self % (Self::one() + Self::one()) == Self::zero()
    }
}

which of course does not compile. Thanks for your explanation.

Exactly my understanding. Luckily I got this right :slight_smile:

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.