rusttypestype-conversiontraits

How can I make a trait that allows conversion between all types implementing it?


I'm writing a library that deals with numerical attributes. For this I have created a trait for all the number primitives:

trait Sealed {}

#[allow(private_bounds, reason = "To seal trait Number")]
pub trait Number: Sealed
    + Sized + Clone + Copy
    + Add<Output = Self>
    + Sub<Output = Self>
    + Mul<Output = Self>
    + Div<Output = Self>
{}

Assume that this trait is implemented for all numerical types (u8, u16, etc...). It's also sealed so that no other type can implement it.

Naturally, you can convert numerical primitives from and to each other using the as keyword, it's a simple cast. However, if all we know about a type T is that it implements Number, this cast no longer works.

To address this, I have created another trait that allows conversion between Number instances:

pub trait ConvertNumber<T>: Number
where
    T: Number
{
    fn convert(self) -> T;
}

Again, assume that this trait is implemented for every pair of numerical primitives.

This now works, but every time I need conversion between different types that implement Number, I have to add an extra constraint to ConvertNumber, which can lead to the API leaking implementation details.

My question is, can I make the compiler know that every T: Number is convertible to any other type that implements Number without extra constraints?


Solution

  • I was scratching my head for a while, but I found a complete solution to my own problem.

    This solution requires the usage of two more traits, FromPrimitive and ToNumber.

    pub trait ToNumber: Sealed {
        fn convert<T: Number>(self) -> T;
    }
    
    pub trait FromPrimitive: Sealed {
        fn from_u8(n: u8) -> Self;
        fn from_u16(n: u16) -> Self;
        fn from_u32(n: u32) -> Self;
        fn from_u64(n: u64) -> Self;
        fn from_u128(n: u128) -> Self;
        fn from_usize(n: usize) -> Self;
    
        fn from_i8(n: i8) -> Self;
        fn from_i16(n: i16) -> Self;
        fn from_i32(n: i32) -> Self;
        fn from_i64(n: i64) -> Self;
        fn from_i128(n: i128) -> Self;
        fn from_isize(n: isize) -> Self;
    
        fn from_f32(n: f32) -> Self;
        fn from_f64(n: f64) -> Self;
    }
    

    I just needed to implement these new traits for all primitives, then constrain trait Number even further:

    pub trait Number: Sealed
        + FromPrimitive
        + ToNumber
        + Sized + Clone + Copy
        + Add<Output = Self>
        + Sub<Output = Self>
        + Mul<Output = Self>
        + Div<Output = Self>
    {}
    

    With this, I can freely convert between generic instances of Number without requiring any additional constraints on the callsite.