Please can we add a basic num trait?

There's a great debate on a shopping list of things we don't have:

Unless you're pulling in the num-traits crate, life is still pretty unpleasant if you want to do anything generic across all the integer types that rust has. There are hacks to get everything you need but it's ugly and not anything that you would want to show to a beginner. Is there any reason why we can't add a NumInfo trait to give the basics of MAX, MIN, BITS, is_signed(), ZERO, ONE?

Computers are generally used to process numbers and strings so it's helpful to adoption if we can do both of these out of the box without needing external crates.

People say that one and zero are not necessary for std lib but we want people to read rust code and understand it and having zero and one being explicit would make code more readable and simpler to understand.

(If there's a related PR/Issue that I missed please point me at it)

To me this feels like we just have not got round to joining the dots. This feels fairly uncontroversial (waiting to be disabused of this notion!) so I thought I would ask here and find out if there's any reason not to raise a PR so we can see how it feels in nightly...

7 Likes

As well as this being useful for people building on rust, this would enable tidying up some of the code in the std lib itself around number handling.

I want this too, but it sounds like there are some serious practical obstacles, e.g. from the previous thread:

Mind that moving traits to std makes it impossible to add a new required item. Float , Real , and PrimInt contain dozens of required items and it seems likely that we'd want to add new methods in the future.

4 Likes

We could seal them, at least for now if not indefinitely.

There are two separate goals here: having a trait to abstract over the integer types, and having a trait to add new integer types. Solving the former is much easier.

7 Likes

As a possible point of discussion, here is a trait that I wrote in one of my own projects to abstract over the primitive integer types…

pub trait Integer:
  'static
  + num::PrimInt
  + num::Integer
  + num::FromPrimitive
  + num::bigint::ToBigInt
  + AddAssign<Self>
  + SubAssign<Self>
  + MulAssign<Self>
  + WrappingAdd
  + WrappingSub
  + WrappingMul
  + for<'a> Add<&'a Self, Output = Self>
  + for<'a> Sub<&'a Self, Output = Self>
  + for<'a> Mul<&'a Self, Output = Self>
  + CheckedShl
  + CheckedShr
  + Shl<u32, Output = Self>
  + Shr<u32, Output = Self>
  + ShlAssign<u32>
  + ShrAssign<u32>
  + std::iter::Sum
  + Debug
  + Display
  + Default
  + Send
  + Sync
{
  fn saturating_mul(self, other: Self) -> Self;
  fn total_bits() -> u32 {
    mem::size_of::<Self>() as u32 * 8
  }
  fn nonsign_bits() -> u32;
  fn to_big_rational(&self) -> BigRational {
    BigRational::from(self.to_bigint().unwrap())
  }
  fn squared(self) -> Self {
    self * self
  }
}

Obviously, the collection of stuff I dumped in there is pretty ad-hoc, but it would definitely be nice if I didn't have to write down all that stuff to make the other parts of my code be more convenient. And it doesn't even have everything I'd like it to (since you can't express &'a Self: for<'a> Add<&'a Self, Output = Self> as a supertrait, can't write numeric literals with them, etc.)

2 Likes

Is it still unpleasant if you are pulling in the num-traits crate? Being able to release new major versions because it's a crate seems like a huge advantage of it being in a crate, vs std.

If we want to be able to add more things to the trait ever, then I agree it'd have to be sealed in core, which means it's not an extensibility point any more, and thus you still need something like num-traits for all the library code that wants to be able to be generic over custom numeric types too. (For example, uom types as just came up in Working around the orphan rule - help - The Rust Programming Language Forum )

I think the core problem here is that what exactly the basics are isn't that obvious.

For example, NonZeroU32 clearly shouldn't have ZERO. But MAX, MIN, BITS, ONE are all completely plausible for it, so it'd seem a shame to me to say that it's not NumInfo despite being in std::num.

Or how exactly should these be represented? Your list, for example, suggests that is_signed is a method, unlike the others that are all constants. Why is that? Why not IS_SIGNED or a separate trait?

There are also completely different approaches, like the one I experiment with in zero-one — Rust library // Lib.rs, where instead of a Zero trait there's a Zero type (which is zero-sized), and instead of T::zero() one uses T::from(Zero) or Zero.into().

It's also possible to consider the need for a T::ZERO as a language problem, because it's just a hack to work around the fact that 0 doesn't work as one would like. If we could, instead, use CTFE to define some way for types to use literals, then perhaps instead of T: NumInfo, you could use some sort of T: FromIntegerLiteral, and thus 0 would do what people rationally wish it would.

If you'd like a polyfill that works on stable for them:

fn zero<T>() -> T where T: std::iter::Sum {
    std::iter::empty::<T>().sum()
}
fn one<T>() -> T where T: std::iter::Product {
    std::iter::empty::<T>().product()
}
10 Likes

Complete agreement with all of that: I think if we want to do this, we want several integer traits, not just one.

2 Likes

Good point about NonZeroU32 types.

In the spirit of baby steps, what's the smallest trait we could have? Would having MIN, MAX, and BITS as a single trait be a first baby step?

(Is signed is a bit easier to figure out if you can get hold of MIN.)

Something like a num::Bounded with MIN & MAX, maybe? Probably the most useful thing to see would be examples of code that could take advantage of such a trait, to answer questions like "what about num::BigUint that has MIN but not MAX?".

BITS is arguably weird in generic code, since one could say that NonZeroU8 should be 7.994353... bits.

Hmm, maybe there's a nice holistic piece here by having a trait like num::SaturatingFrom (as discussed, IIRC, in https://github.com/rust-lang/rfcs/pull/2484) and a corresponding trait with MIN & MAX that says what the range into which it saturates is.

Arguably if there's no upper bound then it's not Bounded. num::Bounded sounds good.

If we push this to the limits we get two or three traits:

num::LowerBound,

num::UpperBound,

and to be kind to the humans we can add type Bounded = LowerBound + UpperBound. (can we do that in rust yet?)

1 Like

Well, it'd be trait Bounded = LowerBound + UpperBound;, using trait_alias - The Rust Unstable Book -- it's not a type.

2 Likes

Ok, I think this is the lot:

Should I turn this into a PR and we put it under a num_bounds_trait feature flag? (rightly or wrongly, atomicu32 and friends do not have min_value defined upon them.)

I'd suggest asking libs-api what they'd like to see for it. The best place is probably either their zulip stream https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs or making an issue and getting it nominated for a meeting.

For just adding a function here or there, a PR tends to be the right approach. But traits are setting a precedent and asking the ecosystem to adopt them, so they often want an RFC spelling out their intended semantics and the rationale for the design picked.

For example, since we'll have impl const Default or similar for optionally-const defaults, why it this MIN always-constants instead of functions that could also be non-const sometimes. (Not that I'm saying the constants are wrong just that there's a choice to be made.) Or what is Bounded::MIN supposed to mean, and how does that interact with floats? For example, the minimal element of f32 is f32::NEG_INFINITY, not f32::MIN. (And how does the definition not say that NANs are also minima?) For example, std::numeric_limits - cppreference.com has min, lowest, and denorm_min. Should this have all those? Do we need a different version of these for floats? Etc.

Adding a constant for a specific type is easy, but giving something on a trait a meaning such that it can be usefully used generically is hard.

7 Likes

So as another comparison on this note that Swift has both SignedInteger and UnsignedInteger, each of which define max and min constants but have a common parent BinaryInteger which defines neither, and FloatingPoint defines greatestFiniteMagnitude, leastNonzeroMagnitude, and leastNormalMagnitude, etc. which are obviously distinct from the integer constants.

C++'s min looks wrong to me. Our f32::min is their lowest. We're not asking about what's the smallest value that can be represented. If people did want that, that would be a separate floating point specific trait.

I agree that we should probably call them LowerFiniteBound and UpperFiniteBound

zulip thread: rust-lang

Apropos std::numeric_limits, it's worth noting that they're proposing a set of individually specializable type traits to supplement (and probably eventually deprecate) the monolithic numeric_limits.

Semi-relevant: a PR to add checked equivalents of Sum and Product to core:

I get the original impetus for avoiding having something like num-traits in core, but it seems like they've had a long time to bake out-of-core.

num-traits has been versioned v0.2 for over 4 years. Is there really a reason to be concerned about having these traits in core at this point? It seems like they've had a long time to bake and people are generally happy about the shape of them.

Certainly another round of considering the design is in order, and I would expect a long period of such traits being nightly-only, but it seems like it's achievable to eventually stabilize such traits with the consideration that they're effectively immutable and inextensible.

tl;dr: I'm not sure there's much more research in this problem space which will be beneficial versus shipping something that will make most people happy.

Even if such traits were unstable and nightly-only, it seems like having them could greatly improve the internal implementation of the core numeric types, which presently have heavily macro-laden implementations that, in some cases, could be replaced with traits for expressing common functionality instead.

I can't speak for anyone else, but I'm not especially happy with the shape of the num-traits traits – they're certainly useful to me, but as I mentioned in my post upthread, they don't provide nearly the amount of convenience for "just abstract over the primitive integer types" as I would have hoped, partly due to things that could easily be changed, and partly due to lack of compiler support for some of the things you might want.

Speaking of compiler support, if we brought something like NumOps into core, it might be ideal for it to be a trait_alias instead of a separate trait, and that's not stable yet (and I'm assuming wouldn't be backwards-compatible to change). I think there are probably a lot of similar issues, where the ultimate best design for the numeric traits depends on features that are not yet stable (or even not yet settled on by RFC).

On the other hand, I think there are a bunch of single-method traits that don't have any apparent design dilemmas, so it would probably be reasonable to start bringing some of them into core.

4 Likes

Just want to note that some people (including me) are not particularly satisfied with the interfaces defined in num-traits (see here for example). num-traits hasn't changed much for a long time mostly because the maintainer doesn't want to do break changes (which contradicts with the idea that moving features out of std can help the features evolve more freely imo...)

9 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.