Bounds when implementing a trait for a trait

I'd like to create a trait — call it IntWrapper — for a type which wraps an integer. When IntWrapper is satisfied, the concrete type should also automatically implement a bunch of other traits like std::ops::BitOr.

I've succeeded in writing an impl for one specific concrete type, but not in creating a generic impl which will work for all concrete types. Is the problem that I am not specifying the bounds properly in the generic BitOr implementation for the trait IntWrapper?

use std::ops::BitOr;

pub trait IntWrapper {
    // An integer type like `u32`.
    //
    // I chose not to make IntWrapper generic (i.e. `pub trait IntWrapper<T>`)
    // because there must be only a single implementation of IntWrapper for
    // any concrete type.  In other words, a type must not be able to
    // implement both `IntWrapper<u32>` and `IntWrapper<u64>` — it can only
    // implement IntWrapper once, and it must choose a specific type for
    // `InternalStorage`.
    type InternalStorage;

    fn bits(&self) -> Self::InternalStorage;

    fn set_bits(&mut self, bits: Self::InternalStorage);

    fn new() -> Self
    where
        Self: Sized;

    fn from_bits(bits: Self::InternalStorage) -> Self
    where
        Self: Sized,
    {
        let mut wrapper = Self::new();
        wrapper.set_bits(bits);
        wrapper
    }
}

// The test passes if we uncomment this `impl` for the concrete type
// `MyStruct` and comment out the failing generic `impl` below.  However, I'd
// like a generic `impl` instead so that we don't have to create an
// `impl BitOr` for every concrete type.
/*
impl BitOr for MyStruct {
    type Output = Self;

    fn bitor(self, other: Self) -> Self {
        Self::from_bits(self.bits() | other.bits())
    }
}
*/

// How do we specify the bounds on this impl, such that they are satisfied
// for any concrete type which meets the following conditions?
//
// - The size of the concrete type is known at compile time.
// - The type implements the `IntWrapper` trait.
// - The associated type `IntWrapper::InternalStorage` implements `BitOr` and
//   `BitOr<Output = T>` (which will be true for any integer type like `u32`,
//   etc.)
impl<T> BitOr for IntWrapper<InternalStorage = T>
where
    Self: Sized,
    T: BitOr + BitOr<Output = T>
{
    type Output = Self;

    fn bitor(self, other: Self) -> Self {
        Self::from_bits(self.bits() | other.bits())
    }
}

#[derive(Debug, PartialEq)]
struct MyStruct {
    bits: u32,
}

impl IntWrapper for MyStruct {
    type InternalStorage = u32;

    fn new() -> Self {
        MyStruct { bits: 0 }
    }

    fn bits(&self) -> u32 {
        self.bits
    }

    fn set_bits(&mut self, bits: u32) {
        self.bits = bits;
    }
}

#[test]
fn my_struct_bitor_correct() {
    let four = MyStruct::from_bits(0b_0100);
    let eight = MyStruct::from_bits(0b_1000);
    let twelve = MyStruct::from_bits(0b_1100);
    assert_eq!(four | eight, twelve);
}

Playground

PS: My Playground code panics the compiler. :exploding_head:

You would want something like this (note that the Sized bound is implied, so you don't need to write it out):

impl<T> BitOr for T
where
    T: IntWrapper,
    T::InternalStorage: BitOr<Output = T::InternalStorage>

But this is not allowed because of the orphan rule.

Instead of a generic impl, consider implementing the trait for a generic struct: Playground.

1 Like

Reduced (but not necessarily minimal). Did you file a bug?

1 Like

Filed issue 80207.

1 Like

Thanks for the pointer to the orphan rule, which after some research I have come to think of as the "no monkeypatching rule".

If I understand correctly, my approach was doomed because the Rust compiler must prove trait coherence — that there is only ever a single impl which applies for a given type and trait, so that you don't wind up with multiple foreign crates reaching into the same section of somebody else's code and creating conflicting implementations. Hence, "no monkeypatching", since enforcement of trait coherence and the "orphan rule" prevents one form of monkeypatching and inoculates against its notorious downsides:

Pitfalls

Malicious, incompetently written, and/or poorly documented monkey patches can lead to problems[...]

  • If two modules attempt to monkey patch the same method, one of them (whichever one runs last) "wins" and the other patch has no effect, unless monkey patches are written with a pattern like alias_method_chain .[6]

  • They create a discrepancy between the original source code and the observed behaviour that can be very confusing to anyone unaware of the existence of the patch. For example, the Linux kernel detects proprietary and other third-party modules such as the Nvidia driver, which tamper with kernel structures, so that developers will not waste their time trying to debug a problem that they cannot fix.[7]

I had hoped that by limiting IntWrapper to a single integer type for InternalStorage I was giving the compiler enough information to determine that there must be only one possible type which could be used to implement std::ops::BitOr. However, I can accept that I did not make my case convincingly enough to the compiler — there are lots of programs which are valid but that the compiler cannot prove are valid :slight_smile: — and that therefore I need to follow the guideline that "either the type or the trait must be local".

From there, we get to the "newtype pattern" you recommend. That turns out to pose other challenges for my actual use case, but you couldn't have known that because I simplified away the relevant details when preparing my example code. As far as the issue I've raised here, thank you very much for your response, which thoroughly solved the question i posed.