Implementing traits only when trait bounds of associated types are satisfied

When I create a game using Bevy, I need to define a lot of components since Bevy uses a entity-component system.

I tried to create a macro to define simple components with the newtype pattern, but I faced a problem.

In the following code, I want to implement the Min and Default traits for a component only if its inner type implements Min.

macro_rules! define_component {
    ($name:ident, $inner:ty) => {
        // a newtype representing a component
        pub struct $name(pub $inner);

        impl Component for $name {
            type Inner = $inner;
        }

        // I want to implement Min for the component,
        // only when the inner type implements Min.
        impl Min for $name
        where
            <Self as Component>::Inner: Min,
        {
            type Output = Self;

            fn min() -> Self {
                Self(<<Self as Component>::Inner as Min>::min())
            }
        }

        // I want to implement Default for the component,
        // only when Self implements Min.
        impl Default for $name
        where
            Self: Min,
        {
            fn default() -> Self {
                Self::min()
            }
        }
    };
}

// `Inner` is the inner type of the newtype.
trait Component {
    type Inner;
}

trait Min {
    type Output;

    fn min() -> Self;
}

impl Min for u32 {
    type Output = Self;

    fn min() -> Self {
        0
    }
}

// These components are OK since the inner or innermost type u32 implements the Min trait.
define_component!(EnemyCount, u32);
define_component!(MaxEnemyCount, EnemyCount);
define_component!(ItemCount, u32);
define_component!(MaxItemCount, ItemCount);

// This impl is commented out to test the behavior when the Min trait is not
// implemented for the inner or innermost type f32.
//
//impl Min for f32 {
//    type Output = Self;
//    fn min() -> Self {
//        0.0
//    }
//}

// These result in compilation errors since the inner type f32 does not implement Min.
define_component!(Speed, f32);
define_component!(Fuel, f32);

// This results in compilation errors since the innermost type f32 does not implement Min.
define_component!(MaxSpeed, Speed);

I get compilation errors like this:

error[E0277]: the trait bound `f32: Min` is not satisfied
  --> src/lib.rs:13:13
   |
13 |             <Self as Component>::Inner: Min,
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Min` is not implemented for `f32`
...
72 | define_component!(Speed, f32);
   | ----------------------------- in this macro invocation

I want the compiler to avoid implementing traits instead of generating a compilation error when the trait bound for the associated type is not satisfied, is this possible?


For clarity, I'll show the definitions I want;

Components with u32 values should have these definitions since u32 implements Min.

  • struct $name
  • impl Component for $name
  • impl Min for $name
  • impl Default for $name

Components with f32 values should have these definitions since f32 doesn't implment Min.

  • struct $name
  • impl Component for $name

You are trying to put a bound on a concrete type. Naturally, that doesn't work when the bound is not satisfied. impl ConcreteTrait for ConcreteType tells the compiler to unconditionally implement the trait; that you have always-unsatisfiable constraints in the where clause leads to the errors.

It's like trying to compile any other code that has a constraint violation, e.g. a function that has a type error in its body. You wouldn't expect such functions to simply be ignored, either.

Since your impls can be implemented by a macro, that means they are uniform across types. You could therefore create a blanket impl and rely on generics for the constraints:

impl<T> Min for T
where
    T: Component,
    <T as Component>::Inner: Min<Output = Self>,
{
    type Output = Self;

    fn min() -> Self {
        Self(<<Self as Component>::Inner as Min>::min())
    }
}

Unfortunately, this does not work with Default due to coherence.

2 Likes

the macro expanded to concrete types, but the where clause really only work as a selector on generic context, for concrete types, it will works as a assertion instead. e.g.

the Min case is easy, you just move it out of the macro and put a generic blanket implementation somewhere else. but the Default trait is a foreign trait, you cannot make a generic blanket implementation. so you'll have to come up with other solutions, for instance you define your alternative of Default trait.

1 Like

Thanks, I'll use generic blanket impl. :+1: