-
Notifications
You must be signed in to change notification settings - Fork 4.7k
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
INumber or INumeric interface for base number types #14723
Comments
We've explored designs like this internally in the past. One of the sticking points has been you can't define operators on interfaces today, requiring us to use methods like |
cc: @CarolEidt |
You are both right. The issue was defining operators on interfaces, and it is a non-syntactic issue. Operators are static methods (not instance) and interfaces cannot specify a static method. Thus, one can't specify the natural constraints for INumeric that one would like (i.e. that it have the expected operators). Other designs have added Add, Sub, etc. as instance methods, but that requires changing the definition of all the primitive types. I had proposed adding static interface methods some time ago for this very purpose, but it was considered to be too high cost and cross-cutting (language and runtime). |
This would indeed be incredibly useful, but I can understand the difficulties. I wonder if it would be easier to add support for a |
There was some discussion about having operator constraints, e.g. where T: op_Addition (not necessarily this exact syntax). But even this requires runtime support, and doesn't address static properties (e.g. Zero) which would be desirable. It does address the issue of allowing subsets of operators, for different mathematical classes (e.g. Group, Ring, Field), though one could envision hierarchical interfaces for that purpose. |
There are ways to use interfaces, operators and get good performance (to a certain limit depending on JIT's mood) but the solution suffers from at least one usability issue: interface ICalculator<T> {
T Add(T x, T y);
}
struct Int32Calculator : ICalculator<int> {
public int Add(int x, int y) { return x + y; }
}
struct Number<T, TCalc> where TCalc : struct, ICalculator<T> {
T value;
public static implicit operator Number<T, TCalc>(T value) {
return new Number<T, TCalc> { value = value };
}
public static implicit operator T(Number<T, TCalc> number) {
return number.value;
}
static TCalc Calc {
get { return default(TCalc); }
}
public static Number<T, TCalc> operator +(Number<T, TCalc> x, Number<T, TCalc> y) {
return Calc.Add(x, y);
}
}
static T Sum<T, TCalc>(T[] values) where TCalc : struct, ICalculator<T> {
var sum = default(Number<T, TCalc>);
foreach (var value in values)
sum += value;
return sum;
}
static void Main() {
Console.WriteLine(Sum<int, Int32Calculator>(new[] { 1, 2, 3 }));
} The code generated for Sum is identical to the code generated for a non-generic Sum(int[]) method (well, almost - there is a bug in JIT that needs a workaround that's not included in this code). The issue is that to use Sum you have to specify the calculator type as a generic argument. Not exactly pretty and not easy to avoid. Maybe it would work if some kind of default generic arguments are added to the language but that's probably just as complicated as adding |
Similar discussion happening over in Rosyln Issues |
@MaleDong's last example gives me an idea: struct Number<T> {
T value;
public static implicit operator Number<T>(T value) {
return new Number<T> { value = value };
}
public static implicit operator T(Number<T> number) {
return number.value;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Number<T> operator +(Number<T> x, Number<T> y) {
if (typeof(T) == typeof(int)) {
return (T)(object)((int)(object)x.value + (int)(object)y.value);
}
else {
NotSupported();
return default(Number<T>);
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void NotSupported() {
throw new NotSupportedException();
}
}
static T Sum<T>(T[] values) {
var sum = default(Number<T>);
foreach (var value in values)
sum += value;
return sum;
}
static void Main() {
Console.WriteLine(Sum(new[] { 1, 2, 3 }));
} On x86 (yet to check RyuJIT) the code generated for Sum is almost OK:
There are some warts like esi->eax->esi but it's possible that these no longer exist in RyuJIT. In any case, the code doesn't contains interface calls, boxing or other expensive operations. However, this will likely "fail" as soon as you add more numeric types to operator+ as the code size will likely exceed the maximum code size the JIT is willing to inline. |
FWIW, this is the general pattern we use for the IL implementation of Vector (in System.Numerics.Vectors). You're right that we can avoid boxing/interface calls, etc, but also correct in that we probably can't get inlining to work well. For Vector we just rely on the compiler intrinsics to take over, anyways, so it has a special-case solution. |
I did a test with RyuJIT and inlining works well even if all the types requested by @MovGP0 are added to What I really don't like about this kind of implementation is that it is limited to certain numeric types, if the code wasn't written to work with BigInteger then it won't work and there's nothing a user can do about it except creating a different |
while static interfaces or code generation might be helpful, i think it might be much simpler to define methods like |
We don't think using |
Since C# 8.0 allows interfaces with static members is it now possible to resurrect this? Edit: I see that static interface methods doesn't yet include operators, but this is yet another reason to allow operators. |
@karelz as per dotnet/csharplang#52 (comment) and dotnet/csharplang#52 (comment) static operators on interfaces are now supported, so can we get this reopened? |
If the language is capable of doing it (would be nice to verify it first), I am fine to reconsider it. |
I often run into the problem that I want to handle numeric base types in a common way.
Unfortunately, they are missing a common interface to express their similarities. Therefore I propose to create an
INumber
orINumeric
interface on the following numeric base types:System.Decimal
System.Double
System.Single
System.Int32
System.UInt32
System.Int64
System.UInt64
This interface should define common functionality, like
+
,-
,*
,/
, etc.The text was updated successfully, but these errors were encountered: