Is there a way to combine generic trait bounds?

I'm experimenting as I learn, and I've devised a way to create instances of a struct, each of which gets assigned a (semi-)unique ID number, using a generic integer type. This involves both a Thing (which gets assigned the ID) and a Counter (which generates those ID numbers, monotonically increasing), both of which use the same generic type which is meant to be any unsigned integer type, requiring the following set of trait bounds:

num::Integer + num::Unsigned + Copy + std::ops::AddAssign

In the code that I've come up with, I've ended up with this same set of trait bounds written out in six different places. It will compile with fewer, but the set of bounds is logically related, because code that instantiates these types will cause the same concrete type to be used in all cases. Is there any way to write it so that this one set of trait bounds is set in just one place, so that any future change to that set will automatically propagate to every place where it's needed? For example, I might want to expand the set of allowable types by making a small adjustment to the code and using the Clone trait instead of Copy. It would be easy to make an error and not update it everywhere it's needed otherwise.

I'm brand new to Rust programming, and I have a suspicion that I'm just approaching the whole problem the wrong way, with a background of writing mostly object-oriented code without multiple threads. (My immediate thought for solving this problem in C++, for example, would be to use a class variable for the counter, which would get incremented in the constructor—obviously not an option in Rust, and for good reason, but that sort of thing leaves me scratching my head and wondering what the "right" way to design the solution is.)

The usual way to solve this is with a blanket impl:

trait Foo: Bar + Baz + Qux {}
impl<T: Bar + Baz + Qux> Foo for T {}

and then wherever you want to require the Bar + Baz + Qux bound, you instead require Foo.

1 Like

I neglected to mention that I did try that, but I failed to figure out that second line, so I thought I would need to individually write an implementation block for each integer type. It's still two occurrences, rather than the one I was hoping for, but at least they can be placed right next to each other. Thanks!

One day this might be be possible via https://doc.rust-lang.org/nightly/unstable-book/language-features/trait-alias.html, but according to that page what you're wanting here doesn't work currently on nightly either.

The trick is to notice that Foo: Bar + Baz + Qux creates an asymmetric relationship between Foo and the other traits: it merely means that in order to implement Foo, you also need to implement the other 3 traits.

That's only helpful when specifying/enforcing constraints, but types won't just automatically and magically implement the Foo trait, so the first line is not helpful when it actually comes to fulfilling those constraints. Hence, you have to write the second line, which establishes the other direction of the asymmetric relation: whenever a type is Bar + Baz + Qux, it's also a Foo.

Altogether, the two directions of this asymmetric relation comprise a symmetric one, meaning that Foo and Bar + Baz + Qux are now essentially the same thing, with respect to both requiring and fulfilling constraints.


Ever wondered why trait bounds are called "bounds"? Well, the system of types and traits forms a lattice, where and inequalities express "subtrait-supertrait" and "implements" relationships. This is nice because then compiler writers can use abstract algebra and stuff for figuring out type checking in a uniform, provably-correct, and efficient way, since asking "does this type implement this trait" or similar questions simply means checking if T ≤ U for two elements of the lattice.

Therefore, requiring a type to implement a trait simply means to bound (either from below or from above, I forget) the compiler's internal representation of these subtyping relationships w.r.t. that type, and a blanket impl of a trait for a type (satisfying another trait) also means to bound the trait being implemented, although in the opposite direction.

As you might remember from highschool algebra, the relation is antisymmetric, so whenever we have Foo ≤ Bar and Bar ≤ Foo at the same time, this implies that Foo = Bar, which happens when you write the two lines above.

3 Likes

An excellent explanation; thank you for taking the time to write it.

In this case, however, my difficulty was not conceptual; I understood the asymmetric nature of the relationship and the choice of the word “bound” seems obvious and intuitive to me. What I was lacking was the means to express the relationship in the syntax of the language.

The algebraic relationshipxy” is asymmetric, but the notation is not. It is immediately obvious that one should write “yx” if that's what one wants, once the relational operator is understood. But in Rust, though I was able to correctly guess that the first expression would be:

trait Foo: Bar + Baz {}

…the counterpart to this expression is much less intuitive, and more complex, involving an extra token, and two other keywords:

impl<T: Bar + Baz> Foo for T {}

Now that I know how to express it, it's relatively straightforward, and it makes sense that I can't simply reverse the expression à la algebra:

trait Bar + Baz: Foo {} // clearly wrong, but what's the right way?

…but since I'm new to Rust, I didn't know if there was a way to express that relationship, or if there was an even simpler way to express the relationship I was really after, which is of course available in algebra (x = y), but apparently not in Rust. (One could express this kind of thing in other programming languages, for example with a C or C++ preprocessor macro (not that I'm advocating that sort of thing), so it's reasonable to wonder if there's a way to accomplish it in Rust, too).

1 Like

There's one more thing that I neglected in my explanation above, but which might justify the seemingly superficial difference between the syntaxes of the two constructs.

impl syntax works for implementing any trait for any concrete or generic type. But it only provides the above guarantee between traits when the impl is a blanket impl, i.e. it is implemented for all types. For instance, impl<Foo> for ConcreteType doesn't imply equivalence between traits Foo and Bar even though ConcreteType might impl Bar – simply because the impl in this case is not a universally-quantified statement, only an existential one.

And then there is the question of usability as well. When you are not thinking about abstract algebra, it's much more helpful to differentiate between the assertions "requires this trait" and "provides this trait", since they are tied to conceptually different actions ("I am expecting something" vs "I am promising something"), even though the algebraic translation might be a symmetric statement. So at this point this boils down to the language design respecting the human viewpoint rather than the abstract mathematical one.

2 Likes

Just to make sure I'm not missing something, shouldn't that be impl Foo for ConcreteType, (not impl<Foo>…)?

I disagree. While it may seem intuitive and natural to someone who is accustomed to thinking about it this way, that doesn't make the “human viewpoint” as universal as you make it out to be. I can now readily read the statement:

impl<T: Bar + Baz> Foo for T {}

…as “For any type T that implements traits Bar and Baz, also implement trait Foo”. Likewise, I read the first statement:

trait Foo: Bar + Baz {}

…as “Any type that implements trait Foo also implement traits Bar and Baz” (though I'm not quite as confident about the details here at the moment).

Even the combination of these things, though adequate, is not quite what I was hoping for. What I would prefer is the equivalent of this:

type MyType = ConcreteType;

…but for traits, not types:

trait Foo = Bar + Baz;

…which doesn't work, but might someday? (Judging by the dates on the discussion, it seems like I shouldn't hold my breath.)

I see. Indeed, it is a solution, but it is not the solution. The proper solution would be trait aliases, however this is a lucky situation where the workaround is almost perfect, albeit less elegant (at least I never encountered a problem with the blanket impl approach).

Certainly, the blanket impl looks bulletproof to me, and since I can put those two lines right next to each other, it's pretty straightforward to keep them in sync (if I change Copy to Clone, for instance), so it's definitely good enough, and far superior to what I was able to come up with on my own. It just doesn't quite express in code the concept of a single definition.

Now, to move on to other clueless beginner questions… :thinking:

Why not create a macro whose name expresses what you want, which generates both output lines from a single macro-invocation input line? That should give you the conceptual clarity within your code that you indicate you want.

The simple answer is that I don't know how to create a macro yet. The better answer is that I prefer my code to be as straightforward and accessible as possible. I'm totally unfamiliar with Rust macros at this point, and I'm guessing that they won't seem as horrible as they are in C++, but it would be yet another layer of obfuscation that I would rather avoid, at least for now.

1 Like

Here's a very basic macro (playground):

macro_rules! trait_alias {
    ($Trait:ident : $($constraints:tt)*) => {
        trait $Trait : $($constraints)* {}
        impl<T: $($constraints)*> $Trait for T {}
    }
}

trait Foo {}
trait Bar {}

trait_alias! { FooBar: Foo + Bar }
2 Likes

Thanks for the macro example, but that looks far more obtuse than anything I'd want to use. I can make pretty good guesses about most of it, but it's got two apparent "keywords": ident and tt, which just expands the amount of code I need to look up in order to decipher the whole thing. I'd much rather avoid that kind of macro unless it's going to be used many, many times.

Note that Foo: Bar + Baz in a trait definition is a bit misleading, because the real bound it is a shorthand for is

trait Foo
where
    Self: Bar,
    Self: Baz,

And here Self refers to the struct (or enum) that implements Foo, not the trait Foo directly. In fact, bounds cannot be reversed because the left-hand-side must be a struct (or enum) and the right-hand-side must be a trait.

2 Likes

At this stage in your Rust learning you don't need to look up or decipher anything; simply copy the lines of the macro definition verbatim into a module of your program and then, when you want a trait alias, simply invoke the trait_alias!{…} macro as @cuviper did in the post where he gifted you with that macro.

When you do decide to learn about Rust's macro_rules!{…} macros, you'll learn that there are a restricted set of things that a macro_rules macro can match on, including identifiers (ident) and token trees (tt).

Also do note that this kind of "user-made trait aliases" do have some drawbacks:

That is, the following compiles fine:

fn f (_: impl Fn(Vec<()>) -> usize) {}

f(|v| v.len()) // Rust infers that `v: Vec<()>`

But the following fails with a type inference error:

trait MyFn
where
    Self : Fn(Vec<()>) -> usize,
{}
impl<T : ?Sized> MyFn for T
where
    Self : Fn(Vec<()>) -> usize,
{}

fn f (_: impl MyFn) {}

f(|v| v.len()) // Rust **fails to infer** that `v: Vec<()>`
1 Like