What numeric traits make sense for std?

For a second, pretend we have GAT, Const Generics, and whatever your favorite type system feature is.

What numeric traits would make sense to have in std? What numeric traits would make sense to not have in std, but in a "2nd party" (i.e. officially endorced) crate? What numeric traits might be useful but can be relegated to a 3rd party crate?

I'm not really concerned with how the traits would be laid out. I'm interested in what functionality you think deserves to be standardized vocabulary for talking about types in programs.

num-traits is the current go-to for numerical traits, of which I'll reproduce a few highlights below:

  • NumOps: just an alias for Add+Sub+Div+Mul
  • Zero/One: additive and multiplicative identity
  • Float: operations that make sense on a floating point number
  • Real: mathematical real numbers without floating point oddities required
  • PrimInt: trivially copyable, two's compliment integers

I personally think that abstractions for higher level mathematical concepts (rings, graphs, etc) aren't generally applicable enough to live in std, but basic numerics feels simple and universal to potentially deserve a spot in std.

2 Likes

If you're really evil, you can currently get the behavior (specified, even!) of One/Zero by using Iterator::product/sum on an empty iterator.

3 Likes

Note that most of num was in std::num before Rust 1.0, but deemed unready for permanent stabilization that std requires. So that became the external crate, later split into the separate num-traits etc.

2 Likes

Similarly, you can spell successor(x) as (x..).next().unwrap() :grimacing:

5 Likes

For integers you could also use From<bool> instead of One and Zero though that's not implemented for floats, where you can use From<i8> or From<u8>.

At the very least, all the methods of primitive number types should be defined in a trait in the standard library. For example, we could have Pow, Abs, Signum, IsPositive, IsNegative, MinValue, MaxValue traits, and so on.

Or, we could have bigger traits with more than one method, like Signed, Float, Int, Bounded.

Zero/One is nice to have, although I'm not sure if it's important enough to live in std.

1 Like

They might help with making it possible to use Iterator::product() and Iterator::sum() on non-std types.

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.

NumOps I feel is too broad. It requires Mul, Div, and Rem, but these three traits satisfy different properties on integers versus floats.

5 Likes

What I find myself doing is writing some traits so that I can write generic code over the integer primitives. What I would find useful is having one large trait implementing all the functions common to the integers, or maybe having three traits: Int, IntSigned: Int and IntUnsigned: Int to handle methods that depend on signedness.

But then I would expect these traits to incorporate any new methods implemented for primitive integers, so they have to be somehow sealed. That means that for the traits I would like, it wouldn't be possible to implement them for external types.

Traits that can be implemented by external types have a different purpose, and it should always be clear whether a trait is intended to cover as much primitive methods as possible, or whether it is intended to be implemented by external types. These aims go against each other, that's why it needs to be clear which of the two kinds is the aim of a trait.

1 Like

Having numeric traits std could've also helped to reduce number of methods, e.g. Duration has float related methods, and we essentially have to duplicate methods by introducing f32 and f64 variants instead of having a single generic version.

4 Likes

which will only get worse if/when Rust introduces fp16 and/or fp128.

I understand there are technical reasons why this isn't yet so but it'd be brilliant to be able to write mathematical code that's generic over:

  • floats
  • unsigned integers
  • signed integers

Unsigned integers in particular only differ from each other in byte length.

After thinking a bit more about this, this is what think I would like:

  1. Lots of very specific traits, similar to the current Neg, Add, BitAndAssign, etc. These would be for example trait Abs, trait FromStrRadix, trait CheckedAdd and so on. These would already allow code to be generic over primitive integers, as all you would need to do is something like fn foo<I>(i: I) where I: CheckedAdd<Output = I> + Abs<Output = I>.

  2. Not all traits would need to have just a single method. For example CheckedAdd could easily be

    trait CheckedAdd<Rhs = Self> {
        type Output;
        fn checked_add(self, rhs: Rhs) -> Option<Self::Output> {
            let (val, overflow) = self.overflowing_add(rhs);
            if overflow { None } else { Some(val) }
        }
        fn saturating_add(self, rhs: Rhs) -> Self::Output;
        fn wrapping_add(self, rhs: Rhs) -> Self::Output {
            self.overflowing_add().0
        }
        fn overflowing_add(self, rhs: Rhs) -> (Self::Output, bool);
    }
    
  3. As a convenience feature, there could be some very broad traits, which should probably be sealed. They would have all the other relevant little specific traits as supertraits. That way someone could just write something like fn foo<I: Int>(i: I) and use all the features common to all integers. I'm not sure these need to be in std though, they could easily be in a crate.

2 Likes