1 unstable release
0.1.0 | Nov 25, 2024 |
---|
#1 in #supertraits
156 downloads per month
7KB
imply-hack: Implied bounds, since 1.79!
Add implied bounds to your traits by adding Imply
as a super trait:
trait Bound {}
trait MyTrait<T>: Imply<T, Is: Bound> {} // Implies T: Bound
Works with Rust 1.79+.
For more information, see the documentation.
lib.rs
:
Add implied bounds to your traits by adding Imply
as a super trait:
trait Bound {}
trait MyTrait<T>: Imply<T, Is: Bound> {} // Implies T: Bound
Works with Rust 1.79+.
For more information, see Why and How.
The problem
If you're the type of person to get lost deep into generic code, you might have run into something like this:
trait MyTrait<T> {
fn do_the_thing(value: &T);
}
struct Foo;
struct MyFooUser;
impl MyFooUser {
fn use_value<T>(&self, value: &T)
where
Foo: MyTrait<T>
{
Foo.do_the_thing(value)
}
}
fn run<T>(value: &T, user: MyFooUser)
where
Foo: MyTrait<T>,
{
MyFooUser.use_value(&value);
Foo.do_the_thing(&value); // Do it again!
}
Now, this is all well and good. But suppose we now want to make run
generic over any FooUser
.
trait FooUser<T>
{
fn use_value(&self, value: &T);
}
impl<T> FooUser<T> for MyFooUser
where
Foo: MyTrait<T>
{
fn use_value(&self, value: &T) { /* ... */ }
}
Now, suppose that FooUser<T>
only really makes sense when Foo: MyTrait<T>
and, notice that we
use Foo: MyTrait<T>
both in run
and in the implementation of FooUser<T>
.
We're violating one of the most important rules of software development: Dont Repeat Yourself!
Note: It might not seem that big of a deal in this example, but imagine that what I'm representing here as a simple bound is, in fact, decidedly not simple. And that
run
is not just a function, but a trait implementation. One of many.
fn run<T, U>(value: T, user: U)
where
U: FooUser<T>,
Foo: MyTrait<T>, // We really want to get rid of this.
{
user.use_value(&value)
Foo.do_the_thing(&value);
}
If you've run into similar situations before, you might be tempted to do:
trait FooUser<T>
where
Foo: MyTrait<T>
{ /* ... */ }
fn run<T, U>(value: T, user: U)
where
U: FooUser<T>,
{ /* ... */ }
But this does not quite work...
error[E0277]: the trait bound `Foo: MyTrait<T>` is not satisfied
--> src/lib.rs:31:8
|
31 | U: FooUser<T>,
| ^^^^^^^^^^ the trait `MyTrait<T>` is not implemented for `Foo`
|
note: required by a bound in `FooUser`
--> src/lib.rs:26:10
|
24 | trait FooUser<T>
| ------- required by a bound in this trait
25 | where
26 | Foo: MyTrait<T>
| ^^^^^^^^^^ required by this bound in `FooUser`
Congratulations! You just stumbled into RFC 2089: Extended Implied bounds
Possible solutions
- Wait for stabilization (See you in 10 years).
- Grab a copy of rustc, zulip and get coding!
- Bite the bullet, and start typing bounds.
- Rework the entire architecture.
None of these is particularly appealing if you don't want to start a very big side quest.
Or...
- Use this crate.
How it works
Suppose we want MyTrait
to imply T: Bound
.
Rust 1.79 stabilized implied bounds on super traits and, notably, associated bounds of super traits.
We can use this by creating a supertrait for MyTrait
, and then constraining an associated type on
that super trait (which we set equal to T
), such that it satisfies Bound
. This looks like this:
trait Imply {
type Is;
}
trait MyTrait<T>
where
Self: Imply<Is: Bound>,
Self: Imply<Is = T>,
{}
This is still a bit annoying to use. Refining the design a bit we get:
trait Imply<T>: ImplyInner<T, Is = T> {}
trait ImplyInner<T> {
type Is;
}
trait MyTrait<T>: Imply<T, Is: Bound> {}
Then, add a few blanket impls and we have imply_hack
!