Search code examples
genericsrusttraits

cannot implement generic fn for integer types


I am experienced in C++ and started to play around with rust.

Trying to implement some simple generic functions, I got stuck with the following problem:

use std::ops::BitAnd;
use std::cmp::Eq;

fn is_odd_i32(x: u32) -> bool {
    if x & 1_u32 == 1_u32 { true } else { false }
}

fn is_odd<T: BitAnd + Eq>(x: &T) -> bool {
    if (*x & (1 as T)) == (1 as T) { true } else { false }
}

fn main() {
    println!("is_odd -> '{}'", is_odd(&23_u64));
    println!("is_odd -> '{}'", is_odd(&23_u32));
}

The problem seems to be the comparison of the bitwise-and result to 0 or 1. I understand that for this to work, 1 (or 0) must be convertible to type T, but don't know how to implement this. I also tried T::try_from(1_u8).ok().unwrap() but this did not work either.

I don't understand how to solve this...

The errors I get are:

error[E0369]: binary operation `==` cannot be applied to type `<T as BitAnd>::Output`
  --> src/main.rs:27:24
   |
27 |     if (*x & (1 as T)) == (1 as T) { true } else { false }
   |        --------------- ^^ -------- T
   |        |
   |        <T as BitAnd>::Output
   |
help: consider further restricting the associated type
   |
26 | fn is_odd<T: BitAnd + Eq>(x: &T) -> bool where <T as BitAnd>::Output: PartialEq<T> {
   |                                          +++++++++++++++++++++++++++++++++++++++++

error[E0605]: non-primitive cast: `{integer}` as `T`
  --> src/main.rs:27:14
   |
27 |     if (*x & (1 as T)) == (1 as T) { true } else { false }
   |              ^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object

error[E0605]: non-primitive cast: `{integer}` as `T`
  --> src/main.rs:27:27
   |
27 |     if (*x & (1 as T)) == (1 as T) { true } else { false }
   |                           ^^^^^^^^ an `as` expression can only be used to convert between primitive types or to coerce to a specific trait object

BTW, I am just playing around with traits and generics, this is not about the best way to test whether an integer is odd or not.


Solution

  • The primary reason your code can't work is because Rust's generics work on completely different principles than C++'s templates.

    In Rust's generics, the constraints are the only things you are allowed to rely on. So when you write T: BitAnd + Eq that is the extent of T's capabilities. There's nothing here that says T is even numeric-adjacent, for all you know someone implemented & on hashsets for convenience. Thus your casts make absolutely no sense to the compiler (casts are not generic operations in the first place), hence its complaints.

    To use generics for this you need a generic way to express those operations, something like a "numeric tower" which defines all base numerical operations as generics, but way back in the early days the core team decided that wasn't an effort that would be useful for the core language and standard library, and what little there used to be was removed / moved to the num ecosystem. For this purpose, specifically the One trait:

    fn is_odd<T: BitAnd + Eq + One>(x: &T) -> bool {
        *x & T::one() == T::one()
    }
    

    (I removed the useless bits of syntax)

    Of course if you apply it you find a bunch of more issues:

    • BitAnd does not guarantee that its output is in any way related to its input, so you need to constrain that to something which makes sense, one option being to just ask for T & T to return T:

      fn is_odd<T: BitAnd<Output=T> + Eq + One>(x: &T) -> bool {
          *x & T::one() == T::one()
      }
      
    • You can't move stuff out of references, which is exactly what *x tries to do here. You could either restrict x to Copy types (which allows copying out of references), or just not make x a reference, I selected the latter because I don't see the point:

      fn is_odd<T: BitAnd<Output=T> + Eq + One>(x: T) -> bool {
          x & T::one() == T::one()
      }
      

    And there you go, if you fix the callsite to remove the reference it now works. is_odd now works about as flexibly as you might want (which might be completely incoherent if a type happens to implement all three operations in a way which does not make sense, which might be why num has is_odd as a non-defaulted operation of the Integer trait).

    As an aside, in this case your printlns are overly complicated versions of dbg:

        dbg!(is_odd(23_u64));
    

    =>

    [src/main.rs:9] is_odd(23_u64) = true
    

    As a second aside, you could also have gotten away with the conversion version (without using num), but in that case as stated originally you need to tell the compiler exactly what conversion you need, here specifically that T needs to be convertible from a u8, aka T: From<u8>. This alternate version also works in this case

    fn is_odd<T: BitAnd<Output=T> + Eq + From<u8>>(x: T) -> bool {
        x & 1.into() == 1.into()
    }
    

    though not if you try to run it with T: i8 for instance (because you can convert an u8 to an i16, but not to an i8 as half the range is invalid).

    You could use TryFrom for this specific corner case, by similarly setting T: TryFrom<u8> instead. And then either use ok().unwrap(), or just 1.try_into().unwrap() if you bound the error:

    fn is_odd<T>(x: T) -> bool
    where
        T: BitAnd<Output = T> + Eq + TryFrom<u8>,
        <T as TryFrom<u8>>::Error: std::fmt::Debug,
    {
        x & 1.try_into().unwrap() == 1.try_into().unwrap()
    }