RE: Family pattern and “restricting” associated types

This is a reply to Family pattern and “restricting” associated types asked by sampsyo - the original question has been closed but I wanted to share my solution, hence opening a new topic.

For a recap of the problem, either read the original topic or read this code.

The idealized solution, as proposed by quinedot relies on at least two (and possibly three) things:

 // 1. GAT equality       vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
trait MeowFamily: Family<for<T> Assoc<T> = Self::MeowAssoc<T>> {
    type MeowAssoc<T>: Trait<T> + Meow;
}

impl<F> MeowFamily for F
where
    F: Family,
//  2. non-lifetime higher-ranked trait bounds (HRTBs)
    for<T> F::Assoc<T>: Meow,
//  ^^^^^^^^^^^^^^^^^^^^^^^^^
{
    type MeowAssoc<T> = F::Assoc<T>;
}

and the third being constrained non-lifetime binders, so that it would be possible to write for<T where T: Sized> Blah<T> as without this, all trait/struct generics must have no bounds at all, not even the implicit Sized, otherwise the current incomplete non-lifetime binders implementation will complain about unsatisfied trait bounds.

In quinedot's final version, the workaround for 1 was to explicitly declare partial equality on certain types:

trait MeowFamily
where
    Self: Family<Assoc<u32> = Self::MeowAssoc<u32>>,
    Self: Family<Assoc<f64> = Self::MeowAssoc<f64>>,

and the workaround for 3 was to add ?Sized everywhere, and use Box-ed values to hold generics (not ideal.) The full solution is here.

My attempt at answering largely kept the ?Sized everywhere adjustments, and the resulting Box-ing. The part of my solution that was different, is a different workaround for 1: instead of explicitly declaring partial equality on certain types, I used a type-witness helper trait instead.

pub trait TyEq {
    type Rhs: TyEq<Rhs = Self> + ?Sized;
    fn eq_refl(self) -> Self::Rhs where Self: Sized;
    fn eq_refl_inv(rhs: Self::Rhs) -> Self where Self::Rhs: Sized;
    fn ref_eq_refl(&self) -> &Self::Rhs;
    fn ref_eq_refl_inv(rhs: &Self::Rhs) -> &Self;
    fn mut_eq_refl(&mut self) -> &mut Self::Rhs;
    fn mut_eq_refl_inv(rhs: &mut Self::Rhs) -> &mut Self;
}
impl<T: ?Sized> TyEq for T {
    type Rhs = T;
    fn eq_refl(self) -> T where T: Sized { self }
    fn eq_refl_inv(rhs: T) -> Self where T: Sized { rhs }
    fn ref_eq_refl(&self) -> &T { self }
    fn ref_eq_refl_inv(rhs: &T) -> &Self { rhs }
    fn mut_eq_refl(&mut self) -> &mut T { self }
    fn mut_eq_refl_inv(rhs: &mut T) -> &mut Self { rhs }
}

Then the MeowFamily trait can be defied in terms of TyEq, like so

trait MeowFamily: Family {
    type MeowAssoc<T: ?Sized>: TyEq<Rhs = Self::Assoc<T>> + Trait<T> + Meow + ?Sized;
}
impl<F: Family + ?Sized> MeowFamily for F
where for<T> Self::Assoc<T>: Meow
{
    type MeowAssoc<T: ?Sized> = Self::Assoc<T>;
}

Finally, we can use the type-witness methods in order to write the challenge impl block

impl<M: MeowFamily + ?Sized> Wrapper<M>
where M::Assoc<u32>: Sized
{
    fn speak(&self) {
        M::MeowAssoc::ref_eq_refl_inv(&self.int).meow();
        M::MeowAssoc::ref_eq_refl_inv(&self.float).meow();
    }
}

And we get the following successful demonstration:

fn main() {
    let one = Wrapper::<OneFamily>::new();
    let two = Wrapper::<TwoFamily>::new();
    let three = Wrapper::<ThreeFamily>::new();
    
    // one.speak(); // doesn't (and shouldn't) work here, but these do:
    two.speak();
    three.speak();
}

The full version of my final answer is here.

Now the question/challenge I have to yet others, who are struggling with the workarounds to 3: have you come up with any good approaches to work around the lack of constrained non-lifetime binders in a reasonable way?

2 Likes