What to do when I have lots of traits for operating numbers together generically

Suppose I want to create a function that adds two numbers modulo another, but I want to be generic with the types. I'd do something like this:

pub fn add_modulo<T: Add<U, Output = T> + Rem<M, Output = T>, U, M>(t: T, u: U, m: M) -> T {
    (t + u) % m
}

If I didn't need to specify which members can be added/'remmed' with others, I could create a trait with this stuff. However, I can't. For this example, it's clean and readable, but take a look at this one, for a carrying add function:

pub fn carry_add_general<T: PartialOrd<S> + Copy, S: Copy>(
    mut o1: Wrapping<T>,
    o2: Wrapping<S>,
    carry: bool,
) -> (Wrapping<T>, bool)
where
    Wrapping<T>: Add<Wrapping<S>, Output = Wrapping<T>>
        + PartialOrd<Wrapping<S>>
        + Not<Output = Wrapping<T>>,
    Wrapping<S>: Zero + One,
{
    o1 = o1 + o2;
    let num_carry = if carry {
        Wrapping::<S>::one()
    } else {
        Wrapping::<S>::zero()
    };
    let carry_result = (o1 < o2) || (!o1 < num_carry);
    (o1 + num_carry, carry_result)
}

looks horrible. And remember that the situation only gets worse because now every function that calls carry_add_general, has to add ALL of these trait bounds to the types. And I cannot even make a trait for them because I have to be specific on which types and be operated with others and with which operators.

Here's a more detailed and working example: Rust Playground

I feel like this is a good case for AsXXX<T> traits. I've seen comments about creating public APIs that use AsRef<T> instead of &T; because it reduces the number of instantiations (str, &str, String,&String, Box<str>, Box<String>,Cell<str>,RefCell<String>,Atomic<String>,etc all map to a single instantiation of AsRef<str>). So if you had an AsWrapping<T> trait, you might break the recursive definition. You have the insane F*ing complexity here that your A,B,mod are three different types, but you might be able to deal with them as well.

if I make them convertible to a common type, like

pub fn add_modulo<T: Into<u64>, U: Into<u64>, M: Into<u64>>(t: T, u: U, m: M) -> T {

then I cannot create a very strange type like u65 and make add_modulo work.

Also, do you have any suggestions on how to avoid the mess of specifying about which types operate with other types?

You could use one of the traits from num, like Num, NumOps or PrimInt. They specify multiple operators as super traits. You can implement them for your own types too.

but I would still have the mess of doing

where
    Wrapping<T>: Add<Wrapping<S>, Output = Wrapping<T>>
        + PartialOrd<Wrapping<S>>
        + Not<Output = Wrapping<T>>,

and the worst part is that every function that calls this function will also have to have these traits, so it would be a huge mess. Recall that this is just one specific function. For each functions, different combinations of operators will be used, so I can't make this a supertrait

1 Like

Yes, I missed all the different type parameters. Sorry. Seems quite complicated. Does it really need to be? I think normalizing to some common type would be a good strategy, like @maraist suggested.

IMO the practical way to go is to use coarser traits such as T: Num, T: Float etc or define your own similar ones, be they new traits or like trait aliases.

T: Float in particular is not exactly very general or narrow to what you actually need, but it's an example of something practical and useful in some contexts.

Regarding the second example, the bounds

    u64: From<Wrapping<T>>,
    Wrapping<T>: From<u64>,

don’t leave many options for T anymore, do they?

Edit: Actually, even T == u64 might not satisfy those bounds?

yes, that's also part of the problem. When I want to use the core::arch::x86_64::_addcarryx_u64, I need the type to be u64. That's why I wanted to be able to specify individual implementations when needed, but also be generic on T. For example, I want to be able to use core::arch::x86_64::_addcarryx_u64 just for then T=u64 but also implement my own add_carry when T=u8 or T=UserDefinedStruct1.

The problem is that now I cannot even have single carry_add function that calls the right implementations based on some activated features or not, because if I even want to use _addcarryx_u64, then my carry_add would have to have T as convertible to u64

As long as you’re okay with restricting yourself to T: 'static, you can do something like that using the Any trait and <dyn Any>::downcast_ref. For known type parameters, this should be completely optimized at compiler time.

I just noticed another problem when trying to use carry_add_general: The bound T: PartialOrd<S> is – for standard library / primitive integer types – only implemented when they’re the same size. Here’s some example code I wrote before realizing that the case T == u64 doesn’t even allow S being u16 or u32, etc… If you look at the assembly you can see it’s getting properly optimized. Without the separate S, things become only easier; you could directly turn &(Wrapping<T>, Wrapping<T>) into Option<&(Wrapping<u64>, Wrapping<u64>) using downcast_ref.

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.