dotnet / csharplang

The official repo for the design of the C# programming language
11.53k stars 1.03k forks source link

[Proposal] Add interface hierarchy to primitive types #3423

Closed ja72 closed 3 years ago

ja72 commented 4 years ago

The intent is to help with generic constrains, extension methods, and generally handling primitives types.

// Root
public interface IPrimitive : IEquatable { } 

// Numeric types
public interface INumeric : IPrimitive, IComparable { }
public interface IInteger : INumeric { }
public interface IBool : IInteger { }       // bool

// Integer types (signed)
public interface ISigned : IInteger { }
public interface ISByte : ISigned { }       //sbyte
public interface IInt16 : ISigned { }       //short
public interface IInt32 : ISigned { }       //int
public interface IInt64 : ISigned { }       //long

// Integer types (unsigned)
public interface IUnsigned : IInteger { }
public interface IByte : IUnsigned { }      // byte
public interface IUint16 : IUnsigned { }    // ushort
public interface IUint32 : IUnsigned { }    // uint
public interface IUint64 : IUnsigned { }    // ulong

// Floating point types
public interface IFloat : INumeric { }  // floating point numbers
public interface ISingle : IFloat { }       // 32-bit `float`
public interface IDouble : IFloat { }       // 64-bit `double`

// Fixed point types
public interface IDecimal : INumeric { }    // decimal

// Other
public interface IChar : IUint16 { }     // char

The specialized libraries can hook into this to defined other custom types that will play well within the existing framework

public interface IComplex: IFloat { }       
public interface IBigInteger : IInteger { }
public interface IRational : INumeric { }       
public interface ISymbol : INumeric { }
public interface IHalfFloat : IFloat { }

Additionally, it would make sense that arithmetic operators and comparisons be required to be implemented to create concrete types from the INumeric interface.

The addition of the above interfaces and implementation with primitive types would make the CLR far more flexible and extensible for scientific computing applications. It would allow for more generic code writing such as:

public struct Vector3<T> where T: INumeric
{
    public T X { get; }
    public T Y { get; }
    public T Z { get; }

    public Vector3(T x, T y, T z)
    {
        this.X = X;
        this.Y = Y;
        this.Z = Z;
    }
    public static Vector3<T> operator + (Vector3<T> a, Vector3<T> b) 
        => new Vector3<T>(a.X + b.X, a.Y + b.Y, a.Z + b.Z);

    public static Vector3<T> operator * (IReal f, Vector3<T> a)
        => new Vector3<T>(f * a.X, f * a.Y, f * a.Z);
}

and the compiler would know how to bind the arithmetic operators or do the implicit conversions (like from float to double) to make the code work without having to write different versions for different numeric types.

HaloFour commented 4 years ago

Wrapping all of the primitive types with interface implementations would be a CLR concern, you could request that here: https://github.com/dotnet/runtime/issues

However, doing so would have some big disadvantages. Calling a method through an interface like this requires boxing the value type which incurs an allocation on the heap, plus the virtual dispatch of calling the instance member on the interface. Generic specialization on structs may help to avoid some of this.

The C# team is proposing solving these kinds of problems in a different way: https://github.com/dotnet/csharplang/issues/1711 The part that's relevant is under the heading "An example: Numeric abstraction" that defines a numeric monoid.

ja72 commented 4 years ago

The C# team is proposing solving these kinds of problems in a different way: #1711 The part that's relevant is under the heading "An example: Numeric abstraction" that defines a numeric monoid.

Well, at least the team recognize the shortcoming and are working on at. Thanks for the issue# link. I heard of monoids before and had no idea what they were talking about, but I am beginning to understand.

john-h-k commented 4 years ago

// Floating point types public interface IReal : INumeric { } public interface IFloat : IReal { } // float public interface IDouble : IReal { } // double

Nit: should be ISingle as that is the proper name. But also, why would there be IDouble? I get IReal, but surely IDouble will only be implemented by Double

ja72 commented 4 years ago

// Floating point types public interface IReal : INumeric { } public interface IFloat : IReal { } // float public interface IDouble : IReal { } // double

Nit: should be ISingle as that is the proper name. But also, why would there be IDouble? I get IReal, but surely IDouble will only be implemented by Double

IReal is there to distinguish between scalar values and non-scalar such as IComplex or IDual.

Then under IReal you might have ISingle for 32-bit floats, IDouble for 64-bit floats and maybe IExtended for 80-bit floats. You are correct that IFloat might be confusing.

tannergooding commented 4 years ago

IReal also doesn't correctly capture things like NaN. Additionally, depending on who you ask PositiveInfinity and NegativeInfinity may or may not quantify as "real numbers".

ja72 commented 4 years ago

IReal also doesn't correctly capture things like NaN. Additionally, depending on who you ask PositiveInfinity and NegativeInfinity may or may not quantify as "real numbers".

A NaN is an artifact of a concrete type such as float or double, so it should be captured under the corresponding types, and it has nothing to do with contracts the interfaces present.

tannergooding commented 4 years ago

It is not just an artifact, it is a fundamental value of the float and double type and therefore something that could be exposed via any IReal interface that was created from a float, double, or from performing certain operations on them.

It is something that would need to be understood and rationalized with these interfaces if they were ever to become a real thing.

gerhard17 commented 4 years ago

I like the idea that primitive types implement a set of standardized interfaces this could be useful in generic constraints..

...but public interface IComplex: IReal { } (for example) is mathematically wrong. Not every complex value is a real value. (only those with imaginary = 0) Instead the reverse is true: every real value is also a complex one!

Difficult to known in advance, which interface hierachy is needed.

ja72 commented 4 years ago

I like the idea that primitive types implement a set of standardized interfaces this could be useful in generic constraints..

...but public interface IComplex: IReal { } (for example) is mathematically wrong. Not every complex value is a real value. (only those with imaginary = 0) Instead the reverse is true: every real value is also a complex one!

Difficult to known in advance, which interface hierarchy is needed.

You are right in that IReal made it seem like a real number, so I renamed it to IFloat since it represents the common ancestor for all floating-point numbers. In that sense, you represent a complex number as two floating-point numbers, and hence it still has the behavior of floating-point numbers and hence inherits from IFloat.

ja72 commented 4 years ago

Wrapping all of the primitive types with interface implementations would be a CLR concern, you could request that here: https://github.com/dotnet/runtime/issues

Is there a way to migrate the issue, or shall I close and re-post in the CLR hub?

jnm2 commented 4 years ago

@ja72 You might want to read https://github.com/dotnet/runtime/issues/14723 first.

See also https://github.com/dotnet/runtime/issues/25058 and https://github.com/dotnet/runtime/issues/18244.

YairHalberstadt commented 3 years ago

Clsoing as duplicate of dotnet/runtime#14723 and other issues on dotnet/runtime