Generic function with changing blocks

I tried porting a SciPy function to Rust. The result is the function symmetry_state. It receives an array and returns an enum describing the array. It's working, but only for floating point types. I'm unable to make it work for all datatypes because of the abs and epsilon.

For unsigned integers, only the first check is useful. I would however change it to

if arr[ii + size1] != arr[size1 - ii]

For signed integers, both checks are useful, but I would remove the epsilon use again on the second block.

if arr[ii + half] == -arr[half - ii]

I was able to code it, adding a trait and using macros

trait SymmetryCheck {
    fn symmetry_state<A: ??????>(arr: &[A]) -> SymmetryState;
}
// Define some macros
// ...
impl_symmetry_state_for_unsigned!(u8, u16, u32, u64);
impl_symmetry_state_for_signed!(i8, i16, i32, i64);
impl_symmetry_state_for_floats!(f32, f64);

but I don't know how to handle the callers, which would need to be defined like so

A: Num + Signed + Float + FromPrimitive,

to handle my 3 implementations. But this is useless... The only types that implement Num + Signed + Float are f32 and f64. So I'm back to the original problem!

How should I express this function in Rust?

I don't know if there is a word for this, but you could "invert" the API's generics so instead of the symmetry_state() function being generic over some A type, you accept &[Self] as the argument and implement the SymmetryCheck trait for each type that is checkable.

trait SymmetryCheck {
  fn symmetry_state(arr: &[Self]) -> SymmetryState;
}

// in reality, you would use macros to generate these

impl SymmetryCheck for u32 {
  fn symmetry_state(arr: &[Self]) -> SymmetryState {
    do_the_first_check();
  }
}

impl SymmetryCheck for f64 {
  fn symmetry_state(arr: &[Self]) -> SymmetryState {
    do_the_first_check();
    do_the_second_check();    
  }
}

Essentially correct, but I probably wouldn't try to embed knowledge about type into the trait.

Something like that:

#[inline(always)]
pub fn symmetry_state<A: SymmetryStateCheck>(a: A) -> SymmetryState {
    a.symmetry_state()
}

pub trait SymmetryStateCheck {
    fn symmetry_state(self) -> SymmetryState;
}

impl<'a> SymmetryStateCheck for &'a [u32] {
  fn symmetry_state(self) -> SymmetryState {
     …
  }
}

impl<'a> SymmetryStateCheck for &'a [i32] {
  fn symmetry_state(self) -> SymmetryState {
     …
  }
}

pub fn generic_function<F: From<u8>>() -> SymmetryState where for<'a> &'a [F]: SymmetryStateCheck {
    …
}

#[derive(PartialEq)]
pub enum SymmetryState {
    NonSymmetric,
    Symmetric,
    AntiSymmetric,
}

Unfortunately generic functions are joy to use in Rust yet real PITA to define. Chalk was supposed to resolve the issue, but looks like it's development stalled.

Yay, it compiles and works! Thank you. I didn't know this syntax

for<'a> &'a [A]: SymmetryStateCheck,

I can't say I like it, but it is the way it is :slight_smile: As you wrote, the definition is a PITA. 105 lines to define what seems like a simple function. Moreover, there's a drawback. I can't write

// Can be any of the integer types
let arr = arr1(&[2, 8, 0, 4, 1, 9, 9, 0]);
symmetry_state(&arr);

anymore because the compiler MUST know the right type. I understand why the compiler needs to know but this is an effort I don't want to ask the users to make. I wonder, would it be better to define my own trait with my_abs and my_epsilon? That way, I could keep a single function for all types (+ several lines of impl abs and epsilon boilerplate)

This would depend on how similar would your functions be. This is mostly about how easy would it be for you to read the code, optimizer would, in most cases, be able to eliminate useless code.

Just a small warning: if you go that way then you may need to force compiler at some point with #[inline(always)].

The problem here lies with the fact that compiler does all optimizations after inlining. That means that when it observes function which calls bazillion other functions it expects that it would be very heavy.

If you have function which is 100+ lines of source but which is expected to fold into 10 bytes of machine code because of dead code elimination then this approach doesn't work.