How the `generic` represent `OR`?

Hello there. As the title, I want to implement a generic like the one introduced below. It could be signed or unsigned but must be one of the two option. It can be used as a numeric type and it also has the numeric ops.

pub trait I32_or_U32: (num::Signed | num::Unsigned) + num::Num  {

}

Can rust has any ways to impl it or some others ways? Thanks to your time.

Perhaps this?

pub trait Trait<const SIGNED: bool> {}

Thank bro. Can it be used as numeric type ? Oh, It's my mistake, The question still miss some info. The I32_or_U32 also used as a numeric type. It maybe the solution. Thank you anyway.

If I understand this time, there is no way to do that, no.

this is probably an XY problem, the solution you are seeking doesn't seem to solve any practical problem. please describe the problem itself instead of the solution you want to approach.


anyway, you can kind of achieve something similar, but it doesn't add real value, it can only be used as some marker bounds.

first, let's see a failed attemp: this code won't compile due to conflicting blanket impls, as the type checker has no way to understand T: Signed and T: Unsigned are non-overlapping.

trait MyTrait {
    const SIGNED: bool;
}
impl<T> MyTrait for T where T: Signed {
    const SIGNED: bool = true;
}
impl<T> MyTrait for T where T: Unsigned {
    const SIGNED: bool = false;
}

this code, however, does compile because a trait with (const or type, doesn't matter) generic parameters is NOT one single trait per se, but rather a family of traits, and you are adding impls for different member of that family.

trait MyTrait<const SIGNED: bool> {}
impl<T> MyTrait <true> for T where T: Signed {}
impl<T> MyTrait<false> for T where T: Unsigned {}

I don't see any means for this to be useful in generic code, except using it as an marker. the trait itself doesn't define any meaningful generic interface; you must repeat the const generic argument wherever you use the trait in your generic code; besides, since Signed and Unsigned support different operations, your "generic" code isn't really generic at all! you still need to figure out some way to dispatch based on whether it's Signed or Unsigned:

// !!!!!! don't do this !!!!!!
struct Foo<const SIGNED: bool, T: I32_or_U32<SIGNED>> {
    //...
}
impl <const SIGNED: bool, T: I32_or_U32<SIGNED>> Foo<SIGNED, T> {
    fn signed_calcuation(&self) -> Option<T> {
        // this function uses signed operations
        if !SIGNED {
            return None;
        }
        todo!()
    }
    fn unsigned_calculation(&self) -> Option<T> {
        // this function uses unsigned operations
        if SIGNED {
            return None;
        }
        todo!()
    }
}

instead, just use the Signed and Unsigned trait directly when your generic code needs certain operations, it is much cleaner:

struct Foo<T> {
    //...
}
impl<T> Foo<T> {
    fn new(foo: T) -> Self where T: Num {
        Self {
            //...
        }
    }
    fn bar(&self) -> T where T: Signed {
        todo!()
    }
   fn baz(&self) -> T where T: Unsigned {
        todo!()
   }
}
2 Likes

Appreciate for your help, that helps me a lot and I have gained some understanding of how to use the generics properly after reading your comment.

In my situation, I have a function like this. If there is some ways to impl 32Bit_Number then when I could call these functions through a unified name of function.

pub fn is_ascending<T: AsRef<[32_bit_number_type]>>(nums: T) -> bool {}
pub fn is_desecending<T: AsRef<[32_bit_number_type]>>(nums: T) -> bool {}

now in my ways to solve it which defined a macro to generate multiple function with different names.
But there is a small thing that make me feel inconvenient. I could only call it through is_ascending_u32 and is_ascending_i32. For using it, there are more than one function name when make a function calling but not a unified way like is_ascending. If there is a trait T: (Signed | Unsigned) + 32Bit + Num, so there is only one entrance to call the function.

macro_rules! is_sorted_suit {
      ($t:ty, $func1:ident, $func2:ident) => {
              pub fn $func1<T: AsRef<[$t]>>(nums: T) -> bool {
              }
             pub fn $func2<T: AsRef<[$t]>>(nums: T) -> bool {
              }
      };
}

is_sorted_suit!(i32, is_ascending_i32, is_descending_i32);
is_sorted_suit!(u32, is_ascending_u32, is_descending_u32);


let nums = vec![1u32, 2, 3];
is_ascending_u32(&nums);
is_descending_u32(&nums);

let nums = vec![1i32, 2, 3];
is_ascending_i32(&nums);
is_descending_i32(&nums);

I don't know why you need the Signed or Unsigned bounds on this function. Nor do I understand why you only want to implement this function for single precision integer types. But unlike the former bound, this is easily implemented with a marker trait. As long as we can use the < and > operators, we should be able to infer whether a slice is sorted in ascending or descending order. We can use < and > if our type implements PartialOrd<Self>. PartialOrd<Self> is a supertrait of Integer, which we can use as a trait bound instead of Num, to make our bound more precise and restrictive. Here's what I have in mind:

use num::Integer;

trait SinglePrecision {}
impl SinglePrecision for u32 {}
impl SinglePrecision for i32 {}

fn ascending<T: Integer + SinglePrecision>(v: &[T]) -> bool {
    let mut prev = &v[0];
    
    for curr in &v[1..] {
        if curr < prev {
            return false;
        }
        
        prev = curr;
    }
    
    true
}

fn descending<T: Integer + SinglePrecision>(v: &[T]) -> bool {
    let mut prev = &v[0];
    
    for curr in &v[1..] {
        if curr > prev {
            return false;
        }
        
        prev = curr;
    }
    
    true
}

#[test]
fn test_ascending() {
    assert!(ascending(&[0u32, 1, 2, 3, 4]));
    assert!(!ascending(&[0u32, 2, 4, 3, 5]));
    
    assert!(ascending(&[-10i32, -1, 2, 3, 4]));
    assert!(!ascending(&[-4i32, -5, 3, 2, 4]));
}


#[test]
fn test_descending() {
    assert!(descending(&[4u32, 3, 2, 1, 0]));
    assert!(!descending(&[0u32, 1, 2, 3, 4]));
    
    assert!(descending(&[4i32, 3, 2, -4, -10]));
    assert!(!descending(&[-4i32, -5, 3, 2, 4]));
}

Playground.

3 Likes

The vision for the future here is marker_trait_attr - The Rust Unstable Book

Of course, it won't let you implement or use any methods from those traits -- they'd either be potentially-missing or they'd be conflicting -- but if it's only needed as a bound then it'd work.

If you want to actually be able to call methods from then, then you'd need num::Signed ^ num::Unsigned, but we don't have a general "mutually exclusive traits" mechanism right now.

2 Likes

Thanks, Impressive ways !
About why the single precision integer types, because the function it is designed for checking a vector which has a length of six million. The algorithm used _mm_loadu_si128 to only accept 4*32 bits parameter. So there is a boundary about single precision.

1 Like

Ah sure, forgot about explicit SIMD instructions. Thanks for clarifying.

Thank you for providing useful information.This is an interesting new feature. But unfortunately the whole project require the version of rust which is stable now.

I see, you are trying to do something similar to "function overloading" from C++? if so, it is perfectly fine to use a marker trait, purely to group some some types for certain APIs, in which case however, you are not really writing "generic" code in a sense, but it's fine, API ergonomics is a valid concern.

for such marker traits, typically you want to manually (or use macros) add the mark to the types you want to include for the API, you don't need super traits such as Signed, Unsigned, unless you are utilize some of the abstract operations provided by the trait (also, unless you want to use the "sealed trait pattern" to prevent implementation for external types)

for instance, you could probably do something like this:

// marker trait, `Sized` required because of `&[Self]`
pub trait SIMDSortable: Sized {
	fn is_slice_ascending(nums: &[Self]) -> bool;
	fn is_slice_descending(nums: &[Self]) -> bool;
}

// same as your existing macro
macro_rules! impl_sortable {
	($t:ty) => {
		impl SIMDSortable for $t {
			fn is_slice_ascending(nums: &[Self]) -> bool { todo!() }
			fn is_slice_descending(nums: &[Self]) -> bool { todo!() }
		}
	};
}

// add marker to types
impl_sortable!(u32);
impl_sortable!(i32);

// if different types need different implementation, you can't use a macro
// you have to implement manually
/*
impl SIMDSortable for i32 { //...
}
impl SIMDSortable for u32 { //...
}
*/

// "overloaded" function
pub fn is_ascending<T: SIMDSortable>(nums: impl AsRef<[T]>) -> bool {
	T::is_slice_ascending(nums.as_ref())
}
pub fn is_descending<T: SIMDSortable>(nums: impl AsRef<[T]>) -> bool {
	T::is_slice_descending(nums.as_ref())
}

another example, I once need to deal with some FFI libraries which use C style string in their public APIs. for legacy reasons, some libraries explicitly use unsigned char const * and signed char const *, and bingen generated code uses c_uchar and c_schar, since rust CStr::as_ptr() returns *const i8, a pointer cast is unavoidable, in my helper crate, I use a marker trait to prevent the the pointer being casted to irrelevant types.

// helper macro to use string literal
pub trait FFIChar{}
impl FFIChar for c_schar {}
impl FFIChar for c_uchar {}
macro_rules! cstrp {
	($s:literal) => {{
		const S: &'static str = concat!($s, '\0');
		const CS: &'static CStr =
			unsafe { CStr::from_bytes_with_nul_unchecked(S.as_bytes()) };
		fn coerce<Char: FFIChar>(cs: &'static CStr) -> *const Char {
			cs.as_ptr().cast()
		}
		coerce(CS)
	}}
}

// ok:
// C function: int search(unsigned char const *name);
let token = bindings::search(cstrp!("abcdefgh"));

// error:
// C function: void release(void *p);
bindings::release(cstrp!("can't release string literal"));
Sorry for the delay. Thank you for your detailed explanation.

Now I understood that the marker trait is more appropriate in this situation. U are right about that this is not really "generic" code in a sense but something about "API ergonomics".

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.