Associated bounds

I've been looking for a way to represent bounds themselves as things a trait can own, mirroring the way associated types are owned by a trait. A simplified motivating example is this Receiver trait. I want to be able to have specific implementations of Receiver restrict the types they operate on, while keeping those types generic until the function is called.

I want to write this, but it doesn't compile because traits (and other bounds) aren't types:

trait Receiver {
    type Bounds;
    fn offer<T: Self::Bounds>(&self, value: T);
}

struct CloneReceiver;
impl Receiver for CloneReceiver {
    type Bounds = Clone;
    fn offer<T: Self::Bounds>(&self, value: T) {
        let _ = value.clone();
    }
}

The only thing I can find to do is bring the generic up to the implementation rather than the function:

trait Receiver<T> {
    fn offer(&self, value: T);
}

struct CloneReceiver;
impl<T: Clone> Receiver<T> for CloneReceiver {
    fn offer(&self, value: T) {
        let _ = value.clone();
    }
}

This works, but is much less practical, especially when adding a generic lifetime to T. When T is chosen, the lifetime of the method call doesn't exist yet, so can't be expressed.

In languages where traits and types are semantically the same, the top example would work. It seems like a useful thing to be able to do, but maybe I'm missing a more idiomatic Rust approach to this problem?

I think the closest you can get is to have an associated type which is a witness for the desired bounds. For Clone it looks something like this

struct CloneWitness<T>{
    clone : for <'a> fn (x : &'a T) -> T
}
impl <T : Clone> Default for CloneWitness<T>
{
    fn default() -> Self
    {
        CloneWitness{ clone: |x| x.clone() }
    }
}
fn my_clone<'a, T>(x : &'a T)  -> T where CloneWitness<T> : Default
{
    let w : CloneWitness<T> = Default::default();
    (w.clone)(x)
}

Your trait then becomes

trait Receiver {
    type BoundsWitness<T>;
    fn offer<T>(&self, value: T)
        where Self::BoundsWitness<T> : Default;
}

struct CloneReceiver;
impl Receiver for CloneReceiver
{
    type BoundsWitness<T> = CloneWitness<T> ;
    fn offer<T>(&self, value: T)
        where Self::BoundsWitness<T> : Default
    {
        let _ = my_clone(&value);
    }
}

fn main()
{
    let x = CloneReceiver;
    
    // call sites need not mention the witness
    x.offer(String::new());
    x.offer(vec![1,2,3]);
}

The crucial part is that the compiler is satisfied that the bounds Self::BoundsWitness<T> : Default hold at the callsite where T is concrete, but in the generic context, you can have a T which looks "as if" its unbounded, as in fn offer<T>(&self, value: T) ... . This isn't very nice though, since you would need quite ugly bounds .. where Self::BoundsWitness<T> : Default at each generic callsite.

@yuriy0 Thanks for your reply. This is a clever solution, and solves the problem here. I've been trying it out today and unfortunately I think I oversimplified.

I'd additionally like to be able to compose receivers, so as to be able to represent, say, a receiver that requires Clone only if its upstream does. I can't find a way to allow a downstream receiver to specify any requirements about its upstream, without a way to write this:

struct WrappedReceiver<R: Receiver>
  where for <T> R::BoundsWitness<T>: CloneReceiver
{
    inner: R,
}

But until non-lifetime bindings (on structs/impls) are a thing, it doesn't seem possible to restrict a GAT.

Do you think it would be worth an RFC on 'associated traits'? It would be an expansion of trait aliases to scope inside an impl, and might be good for type-level programming (traits not having the constraints of being 'real' like types).

This isn't exactly a useful example, but illustrates what I'm imagining:

trait Foo {
    trait T1;
    trait T2: Self::T1;
}
fn f<T: Foo>(thing: T::T2) where T::T1: Clone {
    let _ = thing.clone();
}

RFC would be useful but prepare to spend insane amount of time discussing limitations.

Because it's extremely very-well known problem (you couldn't even make something that usefully works with both with Rc and Arc without it) but given the fact that limitation is known for almost 20 years and is still not fixed… you can imagine the size of the problem.

Doesn't mean it's totally impossible, the only problem is that Lindy's law tells us we are about 20 years away from being able to tackle that problem… would love to be proven wrong, of course.

Yes, there is no clean way to do this with this style. You may have tried something like this, which doesn't work:

struct TwoWitnesses<A,B>(A,B);
impl<A:Default, B:Default> Default for TwoWitnesses<A,B>
{
    fn default() -> Self
    {
        TwoWitnesses(A::default(), B::default())
    }
}

struct WrappedReceiver<R>(R);
impl<R : Receiver> Receiver for WrappedReceiver<R>
{
    // The witness type "inherits" the witness of R, and adds another witness for some trait "MyTrait"
    type BoundsWitness<T> =
        TwoWitnesses<<R as Receiver>::BoundsWitness<T>, MyTraitWitness<T>>;

    fn offer<T>(&self, value: T)
        where Self::BoundsWitness<T> : Default
    {
        // Doesn't compile
        self.0.offer(value);
    }
}

But the issue is that trait bound resolution is not coherent, so we cannot infer X:Default given TwoWitnesses<X,_>:Default.

As for submitting an RFC, I do believe this would be a good feature to have. Not to discourage you, but please keep in mind that the RFC process is extremely laborious, may take months or years for even small progress, and ultimately even if accepted and agreed upon, still requires a knowledge rust-compiler-writer to take upon themselves the task of implementing it. Furthermore, for type-system related changes (esp. Trait-related) there are already so many RFCs in the pipeline, the relevant teams are surely at capacity. I don't believe that the rust "leadership" sees this obtuse and difficult process as an issue, and those that do cannot improve the situation (since to improve it would mean to provide more financial support for rustc contributors, many of whom are volunteers).

@yuriy0 @khimru Thanks again for your thoughts. I'm contemplating this RFC; I'd definitely like to write it, but I need to understand the context.

What exactly is the problem that this invokes? I might be misunderstanding, but the Rc/Arc problem is mainly about higher-kinded types, isn't it? Is the issue that GATs leave a gap around the Send + Sync bound, which is difficult to pass around generically without this feature?

I was thinking that I'd focus on problems that fit the shape of 'traits with generic functions, where the bounds on the functions need to be constrained differently for different implementations', aka receivers like I described above. So far I've got the following in that category:

  • Serialisers that put different bounds on the things they can serialise
  • Schedulers that might or might not pass futures between threads, and particularly libraries that require a generic scheduler whose bounds are capped

But perhaps I should be including some of the long-standing problems, if this would solve those too.


I think the scope could be limited to the following.

  1. Extend trait aliases to associated scopes (those that currently permit associated types) following the pattern of associated types.
    trait Thing { trait Tr; fn apply<S: Tr>(&self, s: S); }
    impl Thing for ... { trait Tr = Send; ... }
  2. Add where syntax to compare traits: where Tr1 <: Tr2, meaning where all types that implement trait Tr1 also implement trait Tr2.
    fn do_something<T: Thing>(thing: T) where Send + Sync <: T::Tr { thing.apply(true); }
    The above would work because the compiler would know that bool meets the bound of Send + Sync, so it must meet the bound of T::Tr.

The following would not be included at this stage:

  1. Generic associated traits: trait Tr<T> = Deref<T>
  2. Use of generic types in generic position: impl<T> ... { trait Tr = Deref<T> } (concrete types in generic position would be fine)
  3. Trait generics: struct A<'a, T, trait Tr> { ... }
  4. Lifetimes anywhere
    Lifetimes are not traits, so they shouldn't be included in the bounds here. I think it might also be reasonable to say that any generic lifetimes in traits included in the bounds must be occupied by '_. This would require some thought though.

I think (5) and (6) would want to be added in due course, and (7) and (8) would probably never be desirable. Without these though, perhaps the consequences for the compiler might be reduced.

Yes. And you want to add them through backdoor.

The issue is that GATs had to play some clever tricks to be implementable without higher-kinded types. The same would need to happen to your RFC, too.

Yup. You, essentially, want to introduce higher kinded types without calling them higher kinded types… as such syntax is the least of your worries. The main issue would be to decide hot that works with type resolver. GATs needed to play four years (or more? I don't remember) of games to fit into the trait resolver. You want to extend it further.

Yes. But you would need to talk to the compiler guys and find out if it would be reduced enough to make plan workable. There's a forum for that. You can find a lot of prior art there: as I have said it's something highly desirable, yet, simultaneously, highly complicated thing.

@khimru Understood. Thanks for your help. I'll do some more research and decide whether to pursue this further.