Family pattern and "restricting" associated types

I'm running into a pretty subtle issue with using the "family pattern" for higher-kinded polymorphism. I'm a little worried I'm just taking this whole polymorphism thing too far, and I fully expect that the right answer here is to back down and duplicate a little code… but I also get the sense that I might be missing something obvious.

Here's a playground with a fully worked example. But it's kind of long and hairy, so I'll try to introduce it piece by piece here.

The short version is: I have a Wrapper type that needs to be polymorphic over a polymorphic container type. I'm using GATs and the family pattern to provide that polymorphic type; let's call it Assoc<T>. I'd like to write an impl for Wrapper that applies only in cases when Assoc<T> implements some other trait (in my example called Meow).

The long version starts like this: let's first declare three generic structs.

struct One<T>(T);
struct Two<T>(T);
struct Three<T>(T);

All three implement some trait Trait<T>, for which the details don't really matter. In order to implement my Wrapper, we use the family pattern to provide the three options for the generic type:

trait Family {
    type Assoc<T>: Trait<T>;
}
struct OneFamily;
struct TwoFamily;
struct ThreeFamily;
impl Family for OneFamily {
    type Assoc<T> = One<T>;
}
impl Family for TwoFamily {
    type Assoc<T> = Two<T>;
}
impl Family for ThreeFamily {
    type Assoc<T> = Three<T>;
}

Now we can implement Wrapper. It has two fields, both of which use F::Assoc<T> (which is why we need HKT and the family pattern in the first place):

struct Wrapper<F: Family> {
    int: F::Assoc<u32>,
    float: F::Assoc<f64>,
}

So far so good! However, next let's assume that just two of the underlying polymorphic structs implement some trait, Meow:

trait Meow {
    fn meow(&self) {
        println!("meow!");
    }
}
impl<T> Meow for Two<T> {}
impl<T> Meow for Three<T> {}

Finally, we return to Wrapper. I would love to write an impl for Wrapper that covers the cases when F::Assoc is either Two or Three but not One, i.e., that works whenever F::Assoc<T> is Meow. It's of course possible to do this for a specific family, like this:

impl Wrapper<TwoFamily> {
    fn speak(&self) {
        self.int.meow();
        self.float.meow();
    }
}

…but I can't figure out how to write a single impl that covers both cases, without copying & pasting the impl for the two cases. This is not allowed, of course:

impl<T, F: Family> Wrapper<F> where F::Assoc<T>: Meow {}

because T is unconstrained. (And in any case, if we could do this kind of thing, we wouldn't need families in the first place.)

I keep on thinking there ought to be a way to define a trait SubFamily that somehow "restricts" the Family::Assoc<T> associated type so that it is Trait<T> + Meow, but I can't finagle a way to write that trait.

Thank you for reading to the end of this long and confusing story!

TL;DR: I don't think it's possible today. It's possible to a limited extent on nightly with incomplete features and other undesirable limitations. Here's my final result (which is also the last link in this comment) if you want a sneak-peek; how I got there is explained below.

I would not be surprised if there are some hacky workarounds, though. Maybe you'd be okay with them. But I don't explore that option in this comment.

Based on how you can do this with non-generic associated types, I think you really want two 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 perhaps a (3), which I'll mention in a moment. There's also a partial workaround for (1).

We may have these eventually, but they're likely quite a ways off.


I do think we're inching towards the second, as for<T> is accepted in nightly -- under a feature gate marked as incomplete. Here's a playground with the interesting parts starting on line 77. However, note how it doesn't seem to actually work when you try to compile it.

error[E0599]: the method `speak` exists for struct `Wrapper<TwoFamily>`, but its trait bounds were not satisfied
   --> src/main.rs:102:9
    |
51  | struct TwoFamily;
    | ---------------- doesn't satisfy `TwoFamily: MeowFamily`
...
64  | struct Wrapper<F: Family> {
    | ------------------------- method `speak` not found for this struct
...
102 |     two.speak();
    |         ^^^^^ method cannot be called on `Wrapper<TwoFamily>` due to unsatisfied trait bounds
    |

But the notes contain a clue to the reason:

note: trait bound `T: Sized` was not satisfied
   --> src/main.rs:43:6
    |
43  | impl<T> Meow for Two<T> {}
    |      ^  ----     ------
    |      |
    |      unsatisfied trait bound introduced here
note: trait bound `T: Sized` was not satisfied
...

I'm pretty sure what's going on is that for<T> has no implicit Sized bound, and probably doesn't directly support bounds at all, just like for<'a>. So you will also need:

  1. Constrained binders (for<T where T: Sized>) and/or some form of implied bounds beyond what we have today

If I remove the implied Sized bounds everywhere (and add Boxes where necessary as a result), then the subtrait implementation works. However, you still can't really utilize it as-is, because you don't have (1) equality between the GATs.

But there is a workaround for (1): you actually can declare partial equality:

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

As the playground shows, the partial equality is good enough for your OP (with the addition of mandatory Boxing, which I'm sure you don't want).

The weird phrasing is because you can't include more than one equality constraint inline:

// Doesn't compile
trait MeowFamily: Family<
    Assoc<u32> = Self::MeowAssoc<u32>,
    Assoc<f64> = Self::MeowAssoc<f64>,
>
1 Like

I started again with hacky workarounds in mind, but having worked through the nightly version, I realized that this works for the OP (and isn't what I would call hacky):

impl<F> Wrapper<F>
where
    F: Family,
    F::Assoc<u32>: Meow,
    F::Assoc<f64>: Meow,
{
    fn speak(&self) {
        self.int.meow();
        self.float.meow();
    }
}

It's not a fully general solution, so may or may not work for your actual use case.

1 Like

Awesome! Thank you so much for your insights here. This really clarifies what I was looking for, which (as you can tell) I wasn't so sure of myself.

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.