On Generic associated enum, and type comparisons

Hi!

I have to implement a data-structure that has to support two different bit-orders that have to be specified at compile time and I'm fighting the type system to make it work while being readable and not using any unstable feature.

I've tried the following approaches:

  1. Generic const enum: But adt_const_params is still an incomplete feature :confused:
#![feature(adt_const_params)]

#[derive(PartialEq, Eq)]
enum BitOrder {
    M2L,
    L2M,
}

struct Reader<const E: BitOrder>;

fn main() {
    let r = Reader::<{BitOrder::M2L}>;
}
  1. Generic const bool: But less readable than an enum :confused:
struct Reader<const M2L: bool>;
  1. Just having two structs: But code duplication :confused:
struct ReaderM2L;
struct ReaderL2M;
  1. Like bit_vec, having a marker type:
trait BitOrder{}

struct M2L;
struct L2M;

impl BitOrder for M2L {}
impl BitOrder for L2M {}

struct Reader<E: BitOrder>(std::marker::PhantomData<E>);

fn main() {
    let r = Reader::<M2L>;
}

For now #4 has been the best approach, but now I've found a problem.
Different bit orders implies different operations to do on drop and drop can't be specialized.

My current plan is to be able to implement a comparison function between M2L and L2M and this is the best I come up with:

/// Inner private trait used to remove the possibility that anyone could 
/// implement [`BitOrder`] on other structs
#[allow(private_in_public)]
trait BitOrderCore {}
impl<T: BitOrderCore> BitOrder for T {}

/// Marker trait to require that something is either [`L2M`] or 
/// [`M2L`]
pub trait BitOrder: BitOrderCore {}

/// Marker type that represents LSB to MSB bit order
pub struct L2M;
/// Marker type that represents MSB to LSB bit order
pub struct M2L;

impl BitOrderCore for L2M {}
impl BitOrderCore for M2L {}

/// A trait for marker types comparison
trait BitOrderCmpCore<O: BitOrder>: BitOrderCore {
    fn inner_cmp() -> bool {
         // this is needed because I don't know a way to tell the type system that
         // this trait will have to be impelmented for every variant.
        unimplemented!() 
    }
}
impl BitOrderCmpCore<L2M> for L2M {
    fn inner_cmp() -> bool {
        true
    }
}
impl BitOrderCmpCore<M2L> for M2L {
    fn inner_cmp() -> bool {
        true
    }
}
impl BitOrderCmpCore<L2M> for M2L {
    fn inner_cmp() -> bool {
        false
    }
}
impl BitOrderCmpCore<M2L> for L2M {
    fn inner_cmp() -> bool {
        false
    }
}

/// The user facing trait
pub trait BitOrderCmp: BitOrderCmpCore<M2L> + BitOrderCmpCore<L2M> {
    fn cmp<A: BitOrder>() -> bool 
    where
        Self: BitOrderCmpCore<A> 
    {
        <Self as BitOrderCmpCore<A>>::inner_cmp()
    }
}

This way I can implement the drop function like:

impl<B: BitOrderCmp> std::ops::Drop for Reader<B> {
    fn drop(&mut self) {
        if B::cmp::<M2L>() {
            // M2L logic
        } else {
           // L2M logic
        }
    }
} 

My question is: Is there a less cursed way to do this?

Thanks

I'm not sure why you think this is "cursed"; it is the whole point of generics that different types provide the same interface but a different implementation.

I'm a bit lost as to why you have so many traits though; instead of an explicit comparison (whch is akin to the "downcasting" or "type switching" anti-pattern in OO languages), you should just implement the drop logic on the marker type itself. It should be as easy as this:

/// Marker trait to require that something is either [`L2M`] or 
/// [`M2L`]
pub trait BitOrder: Sized {
    fn drop_impl(reader: &mut Reader<Self>);
}

/// Marker type that represents LSB to MSB bit order
pub struct L2M;
/// Marker type that represents MSB to LSB bit order
pub struct M2L;

impl BitOrder for L2M {
    fn drop_impl(reader: &mut Reader<Self>) {
        // L2M logic
    }
}
impl BitOrder for M2L {
    fn drop_impl(reader: &mut Reader<Self>) {
        // M2L logic
    }
}

impl<B: BitOrder> std::ops::Drop for Reader<B> {
    fn drop(&mut self) {
        B::drop_impl(self);
    }
}
1 Like

Thank you! yeah, my point is to try to avoid the explicit type comparison.
I didn't implement it on the structs L2M and M2L because they don't have to know about the Reader struct, as there might be multiple structs that need the same trick.

But i guess that for now, creating a private trait like ReaderDropImpl might solve this problem.

PS: The point of the multiple traits is just to make it impossibile for anyone else to implement the trait for some other struct, which I think is reasonable and will help avoid bugs as it's basically a boolean maker

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.