I would like to have a couple of traits with some default implementations (hard requirement), calling each other. Of course, a straightforward approach does not work, because trait A has trait B as a parent which has trait A as a parent... Turtles all the way down. What should I do? (I cannot join the traits together, they are supposed to be in different files.)
Non-working code, demonstrating the problem, would be
use std::fmt::Display;
trait A: B + Display {
fn a(&self, count: i32) -> String {
if count == 0 {
String::new()
} else {
format!("[{}]a{}", self, self.b(count - 1))
}
}
}
impl A for String {}
trait B: A + Display {
fn b(&self, count: i32) -> String {
if count == 0 {
String::new()
} else {
format!("[{}]b{}", self, self.a(count - 1))
}
}
}
impl B for String {}
fn main() {
println!("{}", "Hello".to_string().a(3));
}
Depending on the actual end goal, then it can be easier to do what you're doing, by using auxiliary traits along with blanket implementations.
In short, instead of doing this:
trait Foo {
fn foo(&self) {
println!("default foo");
}
}
impl Foo for String {}
Then you introduce a trait HasFoo
and use that to trigger a blanket implementation of Foo
. Which would look something like this:
trait Foo {
fn foo(&self);
}
trait HasFoo {}
impl<T> Foo for T
where
T: HasFoo,
{
fn foo(&self) {
println!("default foo");
}
}
impl HasFoo for String {}
We can do the same to your example, by introducing a trait HasAB
, to trigger a blanket implementation of both A
and B
:
trait HasAB {}
trait A {
fn a(&self, count: i32) -> String;
}
trait B {
fn b(&self, count: i32) -> String;
}
impl<T> A for T
where
T: HasAB + Display,
{
fn a(&self, count: i32) -> String {
if count == 0 {
String::new()
} else {
format!("[{}]a{}", self, self.b(count - 1))
}
}
}
impl<T> B for T
where
T: HasAB + Display,
{
fn b(&self, count: i32) -> String {
if count == 0 {
String::new()
} else {
format!("[{}]b{}", self, self.a(count - 1))
}
}
}
Now you just have to do the following, and your example compiles:
impl HasAB for String {}
Additionally, since we lifted the bounds A: B
and B: A
. Then you can now impl A
for types that don't implement B
, and vice versa. Which you wouldn't be able to do before.
You'll still be able to manually implement each trait. Without forcing that all of them are implemented for all types.
Otherwise, when you're dealing with defaults. Then it's common to move the code into separate functions. Like this:
impl A for Foo {
fn a(&self, count: i32) -> String {
default_a_where_b(self, count)
}
}
fn default_a_where_b<T>(t: &T, count: i32) -> String
where
T: B + Display,
{
if count == 0 {
String::new()
} else {
format!("[{}]a{}", t, t.b(count - 1))
}
}
If you want to get rid of the boilerplate, then you could introduce a macro:
macro_rules! impl_A_where_B {
($t:ty) => {
impl A for $t {
fn a(&self, count: i32) -> String {
default_a_where_b(self, count)
}
}
};
}
and vice versa for impl B
.
Now you simply do:
impl_A_where_B!(Foo);
Since we don't really have specialization nor negative trait bound, then it makes it harder to juggle these cases where we want default implementations and two-way bounds A: B
B: A
.
If it's because you want to require that both traits are implemented, then you can specify that bound on a function instead. Then you avoid the cyclic error:
fn f<T>(t: &T) where T: A + B {}
Overall, Rust doesn't really play nicely (yet?) with what you're trying to do. In reality, you might be better off with a completely separate design. However, it is hard to suggest an alternative solution without a more concrete example.