Skip to content

ACP: primitive numeric traits #371

Open
@CAD97

Description

@CAD97

Proposal

Problem statement

Rust developers, especially those coming from a C++-ish background, often want to write code which is generic over the different primitive numeric types (i.e. iNN, uNN, fNN). That this can't be done without using macro_rules! to paste a trait implementation across has been called "inelegant," and not uncommonly results in beginners trying what seems like an "obvious" axis for generalization being told "you can't do that" for reasons unrelated to what they're trying to accomplish.

The various ecosystem crates for numeric abstraction (e.g. num-traits, funty) clearly illustrate that the desire exists. This ACP doesn't aim to be the final solution, but offers a conservative step forward to solve a clearly defined subset of the problem that will remain useful even if a more general abstraction is added in the future.

Solution sketch

Specifically, we add two sealed and fundamental traits to core::primitive which capture exactly the set of types in the pseudo-types {integer} and {float} (the types given to integer and float literals before type inference). By virtue of being sealed and restricting semantics to just the primitive numeric types, the API signature that should be available on the trait is relatively straightforward — the API which already already exists macro-pasted over each of the primitive numeric types. These traits can be expanded in future releases to expose added inherent functionality

Per this, listed below is just what functionality is simple to add right now and is intended to be further extended.

// mod core::primitive

#[sealed]
#[fundamental]
trait Integer:
    'static + sealed::Sealed
    // core::marker
    + Sized + Copy + Clone + Send + Sync + Unpin
    // core::panic
    + panic::UnwindSafe + panic::RefUnwindSafe
    // core::fmt
    + fmt::Debug + fmt::Display + fmt::Binary + fmt::Octal
    + fmt::LowerHex + fmt::UpperHex + fmt::LowerExp + fmt::UpperExp
    // core::default
    + Default
    // core::cmp
    + PartialEq + Eq + PartialOrd + Ord
    // core::hash
    + hash::Hash
    // core::str
    + crate::str::FromStr
    // core::convert
    + TryFrom<u8> + TryInto<u8>
    + TryFrom<u16> + TryInto<u16>
    + TryFrom<u32> + TryInto<u32>
    + TryFrom<u64> + TryInto<u64>
    + TryFrom<u128> + TryInto<u128>
    + TryFrom<usize> + TryInto<usize>
    + TryFrom<i8> + TryInto<i8>
    + TryFrom<i16> + TryInto<i16>
    + TryFrom<i32> + TryInto<i32>
    + TryFrom<i64> + TryInto<i64>
    + TryFrom<i128> + TryInto<i128>
    + TryFrom<isize> + TryInto<isize>
    // core::ops
    + ops::Add<Output = Self> + for<'a> ops::Add<&'a Self, Output = Self>
    + ops::Sub<Output = Self> + for<'a> ops::Sub<&'a Self, Output = Self>
    + ops::Mul<Output = Self> + for<'a> ops::Mul<&'a Self, Output = Self>
    + ops::Div<Output = Self> + for<'a> ops::Div<&'a Self, Output = Self>
    + ops::Rem<Output = Self> + for<'a> ops::Rem<&'a Self, Output = Self>
    + ops::AddAssign + for<'a> ops::AddAssign<&'a Self>
    + ops::SubAssign + for<'a> ops::SubAssign<&'a Self>
    + ops::MulAssign + for<'a> ops::MulAssign<&'a Self>
    + ops::DivAssign + for<'a> ops::DivAssign<&'a Self>
    + ops::RemAssign + for<'a> ops::RemAssign<&'a Self>
    + ops::Not<Output = Self>
    + ops::BitAnd<Output = Self> + for<'a> ops::BitAnd<&'a Self, Output = Self>
    + ops::BitOr<Output = Self> + for<'a> ops::BitOr<&'a Self, Output = Self>
    + ops::BitXor<Output = Self> + for<'a> ops::BitXor<&'a Self, Output = Self>
    + ops::BitAndAssign + for<'a> ops::BitAndAssign<&'a Self>
    + ops::BitOrAssign + for<'a> ops::BitOrAssign<&'a Self>
    + ops::BitXorAssign + for<'a> ops::BitXorAssign<&'a Self>
    + ops::Shl<u8, Output = Self> + for<'a> ops::Shl<&'a u8, Output = Self>
    + ops::Shl<u16, Output = Self> + for<'a> ops::Shl<&'a u16, Output = Self>
    + ops::Shl<u32, Output = Self> + for<'a> ops::Shl<&'a u32, Output = Self>
    + ops::Shl<u64, Output = Self> + for<'a> ops::Shl<&'a u64, Output = Self>
    + ops::Shl<u128, Output = Self> + for<'a> ops::Shl<&'a u128, Output = Self>
    + ops::Shl<usize, Output = Self> + for<'a> ops::Shl<&'a usize, Output = Self>
    + ops::Shl<i8, Output = Self> + for<'a> ops::Shl<&'a i8, Output = Self>
    + ops::Shl<i16, Output = Self> + for<'a> ops::Shl<&'a i16, Output = Self>
    + ops::Shl<i32, Output = Self> + for<'a> ops::Shl<&'a i32, Output = Self>
    + ops::Shl<i64, Output = Self> + for<'a> ops::Shl<&'a i64, Output = Self>
    + ops::Shl<i128, Output = Self> + for<'a> ops::Shl<&'a i128, Output = Self>
    + ops::Shl<isize, Output = Self> + for<'a> ops::Shl<&'a isize, Output = Self>
    + ops::Shl<Self, Output = Self> + for<'a> ops::Shl<&'a Self, Output = Self>
    + ops::Shr<u8, Output = Self> + for<'a> ops::Shr<&'a u8, Output = Self>
    + ops::Shr<u16, Output = Self> + for<'a> ops::Shr<&'a u16, Output = Self>
    + ops::Shr<u32, Output = Self> + for<'a> ops::Shr<&'a u32, Output = Self>
    + ops::Shr<u64, Output = Self> + for<'a> ops::Shr<&'a u64, Output = Self>
    + ops::Shr<u128, Output = Self> + for<'a> ops::Shr<&'a u128, Output = Self>
    + ops::Shr<usize, Output = Self> + for<'a> ops::Shr<&'a usize, Output = Self>
    + ops::Shr<i8, Output = Self> + for<'a> ops::Shr<&'a i8, Output = Self>
    + ops::Shr<i16, Output = Self> + for<'a> ops::Shr<&'a i16, Output = Self>
    + ops::Shr<i32, Output = Self> + for<'a> ops::Shr<&'a i32, Output = Self>
    + ops::Shr<i64, Output = Self> + for<'a> ops::Shr<&'a i64, Output = Self>
    + ops::Shr<i128, Output = Self> + for<'a> ops::Shr<&'a i128, Output = Self>
    + ops::Shr<isize, Output = Self> + for<'a> ops::Shr<&'a isize, Output = Self>
    + ops::Shr<Self, Output = Self> + for<'a> ops::Shr<&'a Self, Output = Self>
    + ops::ShlAssign<u8> + for<'a> ops::ShlAssign<&'a u8>
    + ops::ShlAssign<u16> + for<'a> ops::ShlAssign<&'a u16>
    + ops::ShlAssign<u32> + for<'a> ops::ShlAssign<&'a u32>
    + ops::ShlAssign<u64> + for<'a> ops::ShlAssign<&'a u64>
    + ops::ShlAssign<u128> + for<'a> ops::ShlAssign<&'a u128>
    + ops::ShlAssign<usize> + for<'a> ops::ShlAssign<&'a usize>
    + ops::ShlAssign<i8> + for<'a> ops::ShlAssign<&'a i8>
    + ops::ShlAssign<i16> + for<'a> ops::ShlAssign<&'a i16>
    + ops::ShlAssign<i32> + for<'a> ops::ShlAssign<&'a i32>
    + ops::ShlAssign<i64> + for<'a> ops::ShlAssign<&'a i64>
    + ops::ShlAssign<i128> + for<'a> ops::ShlAssign<&'a i128>
    + ops::ShlAssign<isize> + for<'a> ops::ShlAssign<&'a isize>
    + ops::ShlAssign<Self> + for<'a> ops::ShlAssign<&'a Self>
    + ops::ShrAssign<u8> + for<'a> ops::ShrAssign<&'a u8>
    + ops::ShrAssign<u16> + for<'a> ops::ShrAssign<&'a u16>
    + ops::ShrAssign<u32> + for<'a> ops::ShrAssign<&'a u32>
    + ops::ShrAssign<u64> + for<'a> ops::ShrAssign<&'a u64>
    + ops::ShrAssign<u128> + for<'a> ops::ShrAssign<&'a u128>
    + ops::ShrAssign<usize> + for<'a> ops::ShrAssign<&'a usize>
    + ops::ShrAssign<i8> + for<'a> ops::ShrAssign<&'a i8>
    + ops::ShrAssign<i16> + for<'a> ops::ShrAssign<&'a i16>
    + ops::ShrAssign<i32> + for<'a> ops::ShrAssign<&'a i32>
    + ops::ShrAssign<i64> + for<'a> ops::ShrAssign<&'a i64>
    + ops::ShrAssign<i128> + for<'a> ops::ShrAssign<&'a i128>
    + ops::ShrAssign<isize> + for<'a> ops::ShrAssign<&'a isize>
    + ops::ShrAssign<Self> + for<'a> ops::ShrAssign<&'a Self>
    // core::iter
    + iter::Sum<Self> + for<'a> iter::Sum<&'a Self>
    + iter::Product<Self> + for<'a> iter::Product<&'a Self>
{
    const MIN: Self;
    const MAX: Self;
    const BITS: u32;

    fn from_str_radix(src: &str, radix: u32) -> Result<Self, ParseIntError>;

    fn count_ones(self) -> u32;
    fn count_zeros(self) -> u32;
    fn leading_zeros(self) -> u32;
    fn trailing_zeros(self) -> u32;
    fn leading_ones(self) -> u32;
    fn trailing_ones(self) -> u32;

    fn rotate_left(self, n: u32) -> Self;
    fn rotate_right(self, n: u32) -> Self;

    fn swap_bytes(self) -> Self;
    fn reverse_bits(self) -> Self;

    fn from_be(x: Self) -> Self;
    fn from_le(x: Self) -> Self;
    fn to_be(self) -> Self;
    fn to_le(self) -> Self;

    fn checked_add(self, rhs: Self) -> Option<Self>;
    fn checked_sub(self, rhs: Self) -> Option<Self>;
    fn checked_mul(self, rhs: Self) -> Option<Self>;
    fn checked_div(self, rhs: Self) -> Option<Self>;
    fn checked_div_euclid(self, rhs: Self) -> Option<Self>;
    fn checked_rem(self, rhs: Self) -> Option<Self>;
    fn checked_rem_euclid(self, rhs: Self) -> Option<Self>;
    fn checked_neg(self) -> Option<Self>;
    fn checked_shl(self) -> Option<Self>;
    fn checked_shr(self) -> Option<Self>;
    fn checked_pow(self, exp: u32) -> Option<Self>;

    unsafe fn unchecked_add(self, rhs: Self) -> Self;
    unsafe fn unchecked_sub(self, rhs: Self) -> Self;
    unsafe fn unchecked_mul(self, rhs: Self) -> Self;
    unsafe fn unchecked_shl(self, rhs: u32) -> Self;
    unsafe fn unchecked_shr(self, rhs: u32) -> Self;

    fn strict_add(self, rhs: Self) -> Self;
    fn strict_sub(self, rhs: Self) -> Self;
    fn strict_mul(self, rhs: Self) -> Self;
    fn strict_div(self, rhs: Self) -> Self;
    fn strict_div_euclid(self, rhs: Self) -> Self;
    fn strict_rem(self, rhs: Self) -> Self;
    fn strict_rem_euclid(self, rhs: Self) -> Self;
    fn strict_neg(self) -> Self;
    fn strict_shl(self, rhs: u32) -> Self;
    fn strict_shr(self, rhs: u32) -> Self;
    fn strict_pow(self, exp: u32) -> Self;

    fn saturating_add(self, rhs: Self) -> Option<Self>;
    fn saturating_sub(self, rhs: Self) -> Option<Self>;
    fn saturating_mul(self, rhs: Self) -> Option<Self>;
    fn saturating_div(self, rhs: Self) -> Option<Self>;
    fn saturating_pow(self, exp: u32) -> Option<Self>;

    fn wrapping_add(self, rhs: Self) -> Option<Self>;
    fn wrapping_sub(self, rhs: Self) -> Option<Self>;
    fn wrapping_mul(self, rhs: Self) -> Option<Self>;
    fn wrapping_div(self, rhs: Self) -> Option<Self>;
    fn wrapping_div_euclid(self, rhs: Self) -> Option<Self>;
    fn wrapping_rem(self, rhs: Self) -> Option<Self>;
    fn wrapping_rem_euclid(self, rhs: Self) -> Option<Self>;
    fn wrapping_neg(self) -> Option<Self>;
    fn wrapping_shl(self) -> Option<Self>;
    fn wrapping_shr(self) -> Option<Self>;
    fn wrapping_pow(self, exp: u32) -> Option<Self>;

    fn overflowing_add(self, rhs: Self) -> (Self, bool);
    fn overflowing_sub(self, rhs: Self) -> (Self, bool);
    fn overflowing_mul(self, rhs: Self) -> (Self, bool);
    fn overflowing_div(self, rhs: Self) -> (Self, bool);
    fn overflowing_div_euclid(self, rhs: Self) -> (Self, bool);
    fn overflowing_rem(self, rhs: Self) -> (Self, bool);
    fn overflowing_rem_euclid(self, rhs: Self) -> (Self, bool);
    fn overflowing_neg(self, rhs: Self) -> (Self, bool);
    fn overflowing_shl(self, rhs: Self) -> (Self, bool);
    fn overflowing_shr(self, rhs: Self) -> (Self, bool);
    fn overflowing_pow(self, exp: u32) -> (Self, bool);

    fn pow(self, exp: u32) -> Self;
    fn isqrt(self) -> Self;
    fn div_euclid(self, rhs: Self) -> Self;
    fn rem_euclid(self, rhs: Self) -> Self;
    fn div_floor(self, rhs: Self) -> Self;
    fn div_ceil(self, rhs: Self) -> Self;

    fn ilog(self, rhs: Self) -> u32;
    fn ilog2(self, rhs: Self) -> u32;
    fn ilog10(self, rhs: Self) -> u32;
    fn checked_ilog(self, rhs: Self) -> Option<u32>;
    fn checked_ilog2(self, rhs: Self) -> Option<u32>;
    fn checked_ilog10(self, rhs: Self) -> Option<u32>;

    fn min_value() -> Self;
    fn max_value() -> Self;
}

#[sealed]
#[fundamental]
pub trait Float:
    'static + sealed::Sealed
    // core::marker
    + Sized + Copy + Clone + Send + Sync + Unpin
    // core::panic
    + panic::UnwindSafe + panic::RefUnwindSafe
    // core::fmt
    + fmt::Debug // + fmt::Display + fmt::LowerExp + fmt::UpperExp
    // core::default
    + Default
    // core::cmp
    + PartialEq + PartialOrd
    // core::ops
    + ops::Add<Output = Self> + for<'a> ops::Add<&'a Self, Output = Self>
    + ops::Sub<Output = Self> + for<'a> ops::Sub<&'a Self, Output = Self>
    + ops::Mul<Output = Self> + for<'a> ops::Mul<&'a Self, Output = Self>
    + ops::Div<Output = Self> + for<'a> ops::Div<&'a Self, Output = Self>
    + ops::Rem<Output = Self> + for<'a> ops::Rem<&'a Self, Output = Self>
    + ops::AddAssign + for<'a> ops::AddAssign<&'a Self>
    + ops::SubAssign + for<'a> ops::SubAssign<&'a Self>
    + ops::MulAssign + for<'a> ops::MulAssign<&'a Self>
    + ops::DivAssign + for<'a> ops::DivAssign<&'a Self>
    + ops::RemAssign + for<'a> ops::RemAssign<&'a Self>
    + ops::Neg
{
    fn is_nan(self) -> bool;
    fn is_sign_positive(self) -> bool;
    fn is_sign_negative(self) -> bool;

    // note: this is limited to current partial f16/f128 support
}

In the future, an extension to coherence that would allow impl<T: Integer> Trait for T and impl<T: Float> Trait for F to be known non-overlapping (e.g. equivalent to using the impl headers for the fundamental trait bound) could make

Alternatives

Doing nothing in std and leaving it to ecosystem crates to provide this functionality is a valid option, and has worked so far.

Instead of using a single trait with both bounds and methods, it could be beneficial to put methods on one trait and expose Integer/Float as trait aliases which include the bound on method availability and all of the other traits. The trait alias form would be able to provide additional &Self: Op<&Self, Output = Self> bounds (while still getting them elaborated at use sites) as well as enable downstreams that want less ambiguous inference around e.g. shifts or conversions to specify fewer bounds.

Splitting methods/bounds has potential technical benefits, but I don't see splitting the functionality further (e.g. to extract a shared supertrait) as being particularly beneficial. Even additional SignedInteger/UnsignedInteger subtraits seem difficult to justify; the goal is not to create a robust modelling of algebraic fields (that would be delegated to ecosystem crates, e.g. num-traits, alga, or simba), but solely to enable the "obvious" generalization over the primitive numeric types that would otherwise have to be done with macros.

Links and related work

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions