Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic number interfaces #50647

Closed
kingcean opened this issue Apr 2, 2021 · 9 comments
Closed

Basic number interfaces #50647

kingcean opened this issue Apr 2, 2021 · 9 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Numerics

Comments

@kingcean
Copy link

kingcean commented Apr 2, 2021

Background and Motivation

There are lots of type about number in .NET so it is hardly to introduce a way to pass these types in our codes except implement lots of overload methods separately for them.
I may code as following, for example, to implement a method to test a number is greater than 0.

public static class A
{
    public static bool IsPositiveNumber(uint i) => i > 0;
    public static bool IsPositiveNumber(int i) => i > 0;
    public static bool IsPositiveNumber(long i) => i > 0;
    public static bool IsPositiveNumber(double i) => i > 0;
    // And for other many many types...
}

In fact, these types are all about number: Int16, Int32, Int64, UInt16, UInt32, UInt64, Decimal, Single, Double, BigInteger, Complex.

Proposed API

Add interfaces for integer and number.

namespace System.Numerics
{
    public interface INumber
    {
        bool IsFixedLength { get; }
        int BitLength { get; }
        bool IsComplex { get; }
    }
    public interface IRealNumber : INumber
    {
        bool IsInteger { get; }  // e.g. (1.0).IsInteger == true
        bool IsFloatingNumber { get; }
        IInteger IntegerPart { get; }  // e.g. (1.0).IntegerPart => 1
        // bool IsComplex => false;
    }
    public interface IInteger : IRealNumber
    {
        BigInteger ToBigInteger();
        long ToInt64();
        int ToInt32();
        bool TryToInt64(out long result);
        bool TryToInt32(out int result);
        // bool IsInteger => true;
        // IInteger IntegerPart => this;
    }
    public interface IFloatingNumber : IRealNumber
    {
        IFloatingNumber FractionalPart { get; }
        double ToDouble();
        bool TryToDouble(out double result);
    }
}

So we can update the current types to implement these interfaces.

namespace System
{
-    public struct Int32 : IComparable, IComparable<int>, IConvertible, IEquatable<int>, IFormattable { }
+    public struct Int32 : IInteger, IComparable, IComparable<int>, IConvertible, IEquatable<int>, IFormattable { }
-    public struct Int64 : IComparable, IComparable<long>, IConvertible, IEquatable<long>, IFormattable { }
+    public struct Int64 : IInteger, IComparable, IComparable<long>, IConvertible, IEquatable<long>, IFormattable { }
-    public struct Double : IComparable, IComparable<double>, IConvertible, IEquatable<double>, IFormattable { }
+    public struct Double : IFloatingNumber, IComparable, IComparable<double>, IConvertible, IEquatable<double>, IFormattable { }
    // ...
}
namespace System.Numerics
{
-    public struct BigInteger : IComparable, IComparable<BigInteger>, IEquatable<BigInteger>, IFormattable { }
+    public struct BigInteger : IInteger, IComparable, IComparable<BigInteger>, IEquatable<BigInteger>, IFormattable { }
-    public struct Complex : IComparable, IComparable<Complex>, IEquatable<Complex>, IFormattable { }
+    public struct Complex : INumber, IComparable, IComparable<Complex>, IEquatable<Complex>, IFormattable { }
}

For example, following is the implementation of Int32.

public struct Int32 : IInteger, IComparable, IComparable<int>, IConvertible, IEquatable<int>, IFormattable
{
    bool IInteger.IsFixedLength => true;
    int IInteger.BitLength => 32;
    bool IInteger.IsComplex => false;
    BigInteger IInteger.ToBigInteger() => new BigInteger(this);
    long IInteger.ToInt64() => this;
    int IInteger.ToInt32() => this;
    bool IInteger.TryToInt64(out long result)
    {
        result = this;
        return true;
    }
    bool IInteger.TryToInt32(out int result)
    {
        result = this;
        return true;
    }
    bool IsInteger => true;
    IInteger IntegerPart => this;
    // Other fields and methods already existed.
}

Usage Examples

So the example about can be like here. It's quite simple and let's say goodbye to those overloads.

public static class A
{
    public static bool IsPositiveNumber(IRealNumber num)
    {
        var i = num.IntegerPart;
        i.TryToInt64(out var l) ? l > 0 : i.ToBigInteger() > 0;
    }
}
@kingcean kingcean added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Apr 2, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Numerics untriaged New issue has not been triaged by the area owner labels Apr 2, 2021
@ghost
Copy link

ghost commented Apr 2, 2021

Tagging subscribers to this area: @tannergooding, @pgovind
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and Motivation

There are lots of type about number in .NET so it is hardly to introduce a way to pass these types in our codes except implement lots of overload methods separately for them.
I may code as following, for example, to implement a method to test a number is greater than 0.

public static class A
{
    public static bool IsPositiveNumber(uint i) => i > 0;
    public static bool IsPositiveNumber(int i) => i > 0;
    public static bool IsPositiveNumber(long i) => i > 0;
    public static bool IsPositiveNumber(double i) => i > 0;
    // And for other many many types...
}

In fact, these types are all about number: Int16, Int32, Int64, UInt16, UInt32, UInt64, Decimal, Single, Double, BigInteger, Complex.

Proposed API

Add interfaces for integer and number.

namespace System.Numerics
{
    public interface INumber
    {
        bool IsFixedLength { get; }
        int BitLength { get; }
        bool IsComplex { get; }
    }
    public interface IRealNumber : INumber
    {
        bool IsInteger { get; }  // e.g. (1.0).IsInteger == true
        bool IsFloatingNumber { get; }
        IInteger IntegerPart { get; }  // e.g. (1.0).IntegerPart => 1
        // bool IsComplex => false;
    }
    public interface IInteger : IRealNumber
    {
        BigInteger ToBigInteger();
        long ToInt64();
        int ToInt32();
        bool TryToInt64(out long result);
        bool TryToInt32(out int result);
        // bool IsInteger => true;
        // IInteger IntegerPart => this;
    }
    public interface IFloatingNumber : IRealNumber
    {
        IFloatingNumber FractionalPart { get; }
        double ToDouble();
        bool TryToDouble(out double result);
    }
}

So we can update the current types to implement these interfaces.

namespace System
{
-    public struct Int32 : IComparable, IComparable<int>, IConvertible, IEquatable<int>, IFormattable { }
+    public struct Int32 : IInteger, IComparable, IComparable<int>, IConvertible, IEquatable<int>, IFormattable { }
-    public struct Int64 : IComparable, IComparable<long>, IConvertible, IEquatable<long>, IFormattable { }
+    public struct Int64 : IInteger, IComparable, IComparable<long>, IConvertible, IEquatable<long>, IFormattable { }
-    public struct Double : IComparable, IComparable<double>, IConvertible, IEquatable<double>, IFormattable { }
+    public struct Double : IFloatingNumber, IComparable, IComparable<double>, IConvertible, IEquatable<double>, IFormattable { }
    // ...
}
namespace System.Numerics
{
-    public struct BigInteger : IComparable, IComparable<BigInteger>, IEquatable<BigInteger>, IFormattable { }
+    public struct BigInteger : IInteger, IComparable, IComparable<BigInteger>, IEquatable<BigInteger>, IFormattable { }
-    public struct Complex : IComparable, IComparable<Complex>, IEquatable<Complex>, IFormattable { }
+    public struct Complex : INumber, IComparable, IComparable<Complex>, IEquatable<Complex>, IFormattable { }
}

For example, following is the implementation of Int32.

public struct Int32 : IInteger, IComparable, IComparable<int>, IConvertible, IEquatable<int>, IFormattable
{
    bool IInteger.IsFixedLength => true;
    int IInteger.BitLength => 32;
    bool IInteger.IsComplex => false;
    BigInteger IInteger.ToBigInteger() => new BigInteger(this);
    long IInteger.ToInt64() => this;
    int IInteger.ToInt32() => this;
    bool IInteger.TryToInt64(out long result)
    {
        result = this;
        return true;
    }
    bool IInteger.TryToInt32(out int result)
    {
        result = this;
        return true;
    }
    bool IsInteger => true;
    IInteger IntegerPart => this;
    // Other fields and methods already existed.
}

Usage Examples

So the example about can be like here. It's quite simple and let's say goodbye to those overloads.

public static class A
{
    public static bool IsPositiveNumber(IRealNumber num)
    {
        var i = num.IntegerPart;
        i.TryToInt64(out var l) ? l > 0 : i.ToBigInteger() > 0;
    }
}
Author: kingcean
Assignees: -
Labels:

api-suggestion, area-System.Numerics, untriaged

Milestone: -

@HaloFour
Copy link

HaloFour commented Apr 2, 2021

IIRC the runtime team attempted to add an IArithmetic interface in the past to make various numeric operations more generic. The problem is that using an interface in this scenario adds a huge amount of overhead compared to the native operations against the primitive types.

The C# language team is actively researching language (and runtime) changes that would enable describing generic constraints over numeric operations in a way that would enable these kinds of scenarios, plus arithmetic operations and a lot more, as zero-cost abstractions. Check out: dotnet/csharplang#164 and dotnet/csharplang#1711.

@alexrp
Copy link
Contributor

alexrp commented Apr 3, 2021

(Also, especially relevant since it's slated for C# vNext: dotnet/csharplang#4436)

@stephentoub
Copy link
Member

Thanks for the suggestion. We won't add this proposal as currently outlined, but @tannergooding has been investigating a set of numerical interfaces as part of dotnet/csharplang#4436.

@Enderlook
Copy link

The C# language team is actively researching language (and runtime) changes that would enable describing generic constraints over numeric operations in a way that would enable these kinds of scenarios, plus arithmetic operations and a lot more, as zero-cost abstractions. Check out: dotnet/csharplang#164 and dotnet/csharplang#1711.

Both issues only improves C#. What about other langauges in .NET? Using interfaces may not be the most performant solution but it will "just work" on all languages inside .NET, such as F# and VB.

IIRC the runtime team attempted to add an IArithmetic interface in the past to make various numeric operations more generic. The problem is that using an interface in this scenario adds a huge amount of overhead compared to the native operations against the primitive types.

What kind of overhead? If you use generics for passing those values (which you will of course do to avoid boxing the interface), the JITter already creates specialized versions of code for each ValueType, so the perfomance of all primitive types (and primitive-like types): Int16, Int32, Int64, UInt16, UInt32, UInt64, Decimal, Single, Double, BigInteger, Complex won't be affected due inlining.

For example look at:

image

In this case the abstraction is zero-cost.

Not sure if with more complex methods the JITter will also inline them, but for simple operations as math ones or the proposed by INumber and friends I'm quite sure it would.

@HaloFour
Copy link

HaloFour commented Apr 5, 2021

@Enderlook

Both issues only improves C#. What about other langauges in .NET? Using interfaces may not be the most performant solution but it will "just work" on all languages inside .NET, such as F# and VB.

Any runtime work to support the language feature would theoretically benefit them all, although some of it may be tied to accompanying language features that each language would need to support. For example the runtime and language are both exploring virtual static methods now which would benefit operators.

In this case the abstraction is zero-cost.

I think part of the problem is that you end up having to walk a very fine line to avoid boxing, virtual calls and the like. I personally don't know the full backstory other than that the runtime team got close enough that it's still commented out in the reference source, but they pulled the plug on it.

@tannergooding
Copy link
Member

Both issues only improves C#. What about other langauges in .NET? Using interfaces may not be the most performant solution but it will "just work" on all languages inside .NET, such as F# and VB.

The features describe general improvements to the .NET ecosystem so that an interface such as the following can be defined:

public interface IAddable<TSelf, TOther>
    where TSelf : IAddable<TSelf, TOther>
{
    static abstract TSelf Zero { get; }

    static abstract TSelf operator +(TSelf lhs, TOther rhs);
}

This would then allow types to implement it as the following:

public struct Int32 : IAddable<int, int>
{
    public static override int Zero => 0;

    public static override int operator +(int lhs, int rhs)
    {
        return lhs + rhs;
    }
}

You could then have a method that uses this such as:

public static class GenericMath
{
    public T Sum<T>(T[] values)
        where T : IAddable<T, T>
    {
        var sum = T.Zero;

        for (int i = 0; i < values.Length; i++)
        {
            sum += values[i];
        }

        return sum;
    }
}

This new cross-cutting functionality would be available to any language that adds support for static abstract members in interfaces but doing so would be up to them.
The JIT, at least for value types, would be able to specialize this and result in very efficient codegen. Reference types should still get the desired functionality but (at least today) won't get specialized.

-- NOTE: This is a minimal sample to represent available functionality and is not necessarily representative of what the actual interface(s) will look like.

@kingcean
Copy link
Author

kingcean commented Apr 6, 2021

IIRC the runtime team attempted to add an IArithmetic interface in the past to make various numeric operations more generic. The problem is that using an interface in this scenario adds a huge amount of overhead compared to the native operations against the primitive types.

IArithmetic only describes about what they can do but not who they are, such as integer, real number, number, etc.

@tannergooding
Copy link
Member

tannergooding commented Apr 16, 2021

Closing this. As mentioned above we are looking into the correct surface area to expose here as part of the static abstracts in interfaces feature the language and runtime are adding.

This is currently being tracked in our dotnet/designs repo and I've added the initial rough draft of the surface area here: dotnet/designs#205

Further discussion or suggestions should happen there and in future PRs that further refine the proposed surface area.

@ghost ghost locked as resolved and limited conversation to collaborators May 16, 2021
@tannergooding tannergooding removed the untriaged New issue has not been triaged by the area owner label Jun 24, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Numerics
Projects
None yet
Development

No branches or pull requests

6 participants