1

I'm trying to write this in Rust:

trait Foo {
    type T: PartialEq;

    fn get_t(&self) -> Self::T;
}

struct Bar<F1: Foo, F2: Foo> {
    f1: F1,
    f2: F2,
}

impl<F1: Foo, F2: Foo> Bar<F1, F2> {
    
    fn test_eq(&self) -> bool {
        self.f1.get_t() == self.f2.get_t()
    }
}

It refuses to compile, because F1::T might be different from F2::T, rendering the equality meaningless. I'd like to be able to resolve that by writing this:

impl<F1: Foo, F2: Foo> Bar<F1, F2>
where
    F1::T = F2::T
{
    
    fn test_eq(&self) -> bool {
        self.f1.get_t() == self.f2.get_t()
    }
}

But that tells me that equality constraints are not yet supported and refers me to this issue. Since it's been open since late 2014, I suspect it's not a feature that's arriving any time soon.

I can do this, and it'll compile:

struct Bar<T: PartialEq, F1: Foo<T=T>, F2: Foo<T=T>> {
    f1: F1,
    f2: F2,
}

impl<T: PartialEq, F1: Foo<T=T>, F2: Foo<T=T>> Bar<T, F1, F2>
{
    
    fn test_eq(&self) -> bool {
        self.f1.get_t() == self.f2.get_t()
    }
}

But then I'm adding an additional generic parameter to the struct Bar. If Foo has multiple associated types that I want to constrain in this manner, then it'll get a bit messy. My question is:

  1. Is this really the only way I can enforce this constraint, by making Bar 'more generic' with the addition of the additional type parameter T?
  2. Is this the most idiomatic way of solving this?

Edit:

I realize I simplified my example too much. Suppose I actually have a Bar that has an associated Baz that has an associated Foo that has an associated T. That's bit of a mouthful, so here is the example:

trait Foo {
    type T: PartialEq;

    fn get_t(&self) -> Self::T;
}

trait Baz {
    type F: Foo;

    fn get_inner_t(&self) -> <Self::F as Foo>::T;
}

struct Bar<B1: Baz, B2: Baz> {
    b1: B1,
    b2: B2,
}

impl<B1: Baz, B2: Baz> Bar<B1, B2>
{
    
    fn test_eq(&self) -> bool {
        self.b1.get_inner_t() == self.b2.get_inner_t()
    }
}

Of course it still doesn't compile because the Ts from get_inner_t can differ. I do genuinely want to allow that the two Bazs differ, and the Foos differ, but constrain their Ts to be the same.

I've tried to adapt the answer given, by writing

impl<B1: Baz, B2: Baz< <F as Foo>::T = B1::F::T >> Bar<B1, B2>

But that gives this compile error:

   Compiling type_bindings v0.1.0 (/home/harry/coding/rust_sandbox/type_bindings)
error: expected one of `(`, `,`, `::`, `<`, or `>`, found `=`
  --> src/main.rs:18:38
   |
18 | impl<B1: Baz, B2: Baz< <F as Foo>::T = B1::F::T >> Bar<B1, B2>
   |                                      ^ expected one of `(`, `,`, `::`, `<`, or `>`

error: aborting due to previous error

error: could not compile `type_bindings`

To learn more, run the command again with --verbose.

If this multi-level extraction of associated types not permitted?

Harry Braviner
  • 627
  • 4
  • 12
  • Putting bounds on the struct `Bar` is probably not what you want, even though it's possible: just put them on the `impl` where they are used, unless `Bar` itself uses `T` in some way. – trent Feb 24 '21 at 22:38
  • I'm not sure that is what I should do. The top answer to this question says you should also implement the bound on the struct if you want it to only hold values that implement that trait (which I do in this case): https://stackoverflow.com/questions/49229332/should-trait-bounds-be-duplicated-in-struct-and-impl Or do you disagree with that answer? – Harry Braviner Feb 24 '21 at 23:51
  • I do disagree -- if you want a type to only hold values that satisfy a particular constraint, it's sufficient to write the constraints on the `impl`s that actually *put* those values inside it. Putting a bound on the type itself imposes a more abstract constraint, that it's actually impossible to *express* the idea of a type that doesn't hold those bounds. People often think they want the broad, abstract constraint when the more specific constraint is sufficient, and they end up writing code that doesn't make sense. – trent Feb 25 '21 at 01:40
  • It's impossible to know whether that's your case or not, but just from participating here and on URLO, I've seen more people confuse themselves by putting the bounds on the type by accident than I have legitimate uses of type bounds on structs -- by at least an order of magnitude. So I think that advice is, not necessarily *wrong*, but likely misleading for most people. If you know what you're doing, you don't need it explained to you. If you don't know what you're doing, bounds on the `impl`s and not on the struct is probably the better choice. – trent Feb 25 '21 at 01:45
  • @trentcl I disagree with your disagreement. In cases where the struct has no use outside of its impls, especially when the struct has private fields, it makes no sense to allow the unbound type to be used, just for the user to find out later that it's a worthless type that can't actually do anything. It's like why `&'a self` is bad: it just gets rid of an error at the site of definition to create a new one at the site of usage. – Aplet123 Feb 25 '21 at 01:55
  • @Aplet123 And I disagree with your counter-disagreement! *Especially* in cases where a struct has no use out of its `impl`s, and **especially** especially in cases where the struct has private fields, are exactly the situations where bounds on the struct are most useless. I've added an unnecessarily long answer at the linked question; feel free to disagree with it there – trent Feb 25 '21 at 13:59

2 Answers2

4

You can "extract" the associated type:

struct Bar<F1: Foo, F2: Foo<T = <F1 as Foo>::T>> {
    f1: F1,
    f2: F2,
}

impl<F1: Foo, F2: Foo<T = <F1 as Foo>::T>> Bar<F1, F2>
{
    
    fn test_eq(&self) -> bool {
        self.f1.get_t() == self.f2.get_t()
    }
}

For your edit, you can use some type aliases to clean up the syntax (not required but is nicer) then use impl or add another generic to the impl (which doesn't make the struct more generic so it doesn't run into the same issues, however it does not let you put the bounds on the struct):

type GetT<T> = <T as Foo>::T;
type GetF<T> = <T as Baz>::F;

impl<B1: Baz, B2: Baz<F = impl Foo<T = GetT<GetF<B1>>>>> Bar<B1, B2>
{
    
    fn test_eq(&self) -> bool {
        self.b1.get_inner_t() == self.b2.get_inner_t()
    }
}

or

impl<T: PartialEq, B1: Baz<F = impl Foo<T = T>>, B2: Baz<F = impl Foo<T = T>>> Bar<B1, B2>
{
    
    fn test_eq(&self) -> bool {
        self.b1.get_inner_t() == self.b2.get_inner_t()
    }
}
Aplet123
  • 33,825
  • 1
  • 29
  • 55
  • Ah, this works, but I realize I'd simplified too far for my example. Would you mind taking a look at the edit I've made and let me know if it's possible to generalize this to that situation? – Harry Braviner Feb 25 '21 at 00:07
  • That solution works, but only for the constraint on the `impl` of `Bar`, not on its struct definition. This isn't a huge limitation, but if it doesn't make sense for `Bar` to exist without that constraint, then do I have a good way to enforce that? – Harry Braviner Feb 25 '21 at 19:25
  • The code block before the "or" lets you put it on both the impl and the struct, whereas the second is only for the impl. – Aplet123 Feb 25 '21 at 19:37
0

While all @Aplet123 noted is valid, I think this is a syntactically neater way of writing it.

trait Foo {
    type T: PartialEq;

    fn get_t(&self) -> Self::T;
}

struct Bar<F1, F2>
where
    F1: Foo,
    F2: Foo<T = F1::T>,
{
    f1: F1,
    f2: F2,
}

impl<F1, F2> Bar<F1, F2>
where
    F1: Foo,
    F2: Foo<T = F1::T>,
{
    fn test_eq(&self) -> bool {
        self.f1.get_t() == self.f2.get_t()
    }
}
trait Foo {
    type T: PartialEq;

    fn get_t(&self) -> Self::T;
}

trait Baz {
    type F: Foo;

    fn get_inner_t(&self) -> <Self::F as Foo>::T;
}

struct Bar<B1, B2>
where
    B1: Baz,
    B2: Baz<F = B1::F>,
{
    b1: B1,
    b2: B2,
}

impl<B1, B2> Bar<B1, B2>
where
    B1: Baz,
    B2: Baz<F = B1::F>,
{
    fn test_eq(&self) -> bool {
        self.b1.get_inner_t() == self.b2.get_inner_t()
    }
}
Ivan C
  • 1,772
  • 1
  • 8
  • 13