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

Generic operators support in C#. #813

Open
gafter opened this issue Aug 11, 2017 · 26 comments
Open

Generic operators support in C#. #813

gafter opened this issue Aug 11, 2017 · 26 comments

Comments

@gafter
Copy link
Member

gafter commented Aug 11, 2017

@dmitriyse commented on Fri Dec 18 2015

Currently c# does not support generic operators definition like that:

public static MyAddExpression<SomeClass, T> operator<T>+(SomeClass c, T t){return ...}

This support can be usefull to build advanced libs with expressions (validation expressions in my case).

This is only C# limitation, CLR ready for this feature.
We can already define operator in different syntax.

[SpecialName]
public  static string op_Addition(SomeClass c, string s){...}

No changes in CLR needed to define generic operator in this syntax:

[SpecialName]
public static MyAddExpression<SomeClass, T> op_Addition<T>(SomeClass c, T t){return ...}

but later C# does not recognize it.

Please add this proposal to the C# 7.0 wish list.


@tpetrina commented on Fri Dec 18 2015

I would love this feature. Non-generic operators are limiting.


@dsaf commented on Sun Dec 20 2015

Related to #3391 and #2147.


@msedi commented on Sat Mar 19 2016

Yes. In my opinion this is now absolutely necessary. Having only generics that are not fully equivalent to C++ generics (in functional behaviour) are absolutely mandatory. I guess there are many guys out there requesting this feature. Instead a lot of people here are complaining about syntactic sugar.


@orthoxerox commented on Tue Apr 05 2016

Might help solve the problem of verbose generic patterns discussed in #10153. If is was generic the pattern could infer its generic type.


@grwGeo commented on Thu Jul 21 2016

I have needed this feature trying to model mathematical entities and it is a definite must. It will save tons of repetitive code and make the conceptual model richer. Also type inference would do wonders in this scenario.

@gafter
Copy link
Member Author

gafter commented Aug 11, 2017

I wonder if this would still be needed if we did type classes (shapes) See #110 and #164

@Richiban
Copy link

Related to this is the generic cast. This is currently not possible in C#, but feels like it should be:

public sealed class NullObject
{
	private NullObject() {}
	
	public static readonly NullObject Instance = new NullObject();

	public static implicit operator List<T>(NullObject o) => new List<T>();
}

Sorry for the dumbness of this example. It's all I could think of right now.

@IanWold
Copy link

IanWold commented Sep 20, 2017

I'm entirely behind this, I've run into plenty of scenarios where this is necessary to reduce bloated and unclear code.

@orthoxerox
Copy link

See also #108, #612

@lostmsu
Copy link

lostmsu commented Sep 15, 2019

Just wanted to bring this up, as shapes and extensions have nothing to do with this feature request, as it talks neither about extending an existing type (extensions), nor about generalizing over static members, which the underlying type system does not support (shapes).

It also seems to work fine in F#, both declaration and consumption, without any special IL generation.

@gafter
Copy link
Member Author

gafter commented Dec 16, 2019

The C# LDM considers it an important feature of the language that every place the language infers a type, you can also explicitly give one. This proposal does not include any way to explicitly provide the type argument at the invocation site. If this proposal were extended to have a way to do that, we don't believe we would like it.

@lostmsu
Copy link

lostmsu commented Dec 17, 2019

@gafter for the explicit type argument would it be possible to simply allow invoking the method via ClassName.op_Addition<T>(..., ...)?

@munael
Copy link

munael commented Dec 17, 2019

Update: Mixed things up badly. Just disregard.
@lostmsu

Just wanted to bring this up, as shapes and extensions have nothing to do with this feature request, as it talks neither about extending an existing type (extensions), nor about generalizing over static members, which the underlying type system does not support (shapes).

It also seems to work fine in F#, both declaration and consumption, without any special IL generation.

At least we'd have a way to abstract over numerical operations (by having an Add static method for every numeric type that maps to +). Or am I missing something deeper?

@gafter

This proposal does not include any way to explicitly provide the type argument at the invocation site. If this proposal were extended to have a way to do that, we don't believe we would like it.

Is this meant as strongly as it reads?

@333fred
Copy link
Member

333fred commented Dec 17, 2019

Is this meant as strongly as it reads?

The proposal was put in the Likely Never milestone, and moved to the Rejected column in LDM triage. It's meant as strongly as it reads.

@gafter
Copy link
Member Author

gafter commented Dec 17, 2019

@gafter for the explicit type argument would it be possible to simply allow invoking the method via ClassName.op_Addition(..., ...)?

"Simply"? Um, That would require a breaking change to name lookup. See
https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgAQEECwAoA3nusesgMykCMAbFugPYAOA+pjDGMGPQHYAUmdAENKUOkNQBKdAF4AfMMoBuPAF88eNOgBCILHkK4SpCsho6GjAKYAnIcHo308PtsVi3E6fMUrc63E0MMwB2AyISNFQI4kNjYzcAI1l0HgBXCAg/eJJtADomVnZObn5EsUTJJXQAehr0AGcAC3oMmHQAYyFMrAKWNg4uXhj0AICgA== for a program that would be broken.

@lostmsu
Copy link

lostmsu commented Dec 17, 2019

@gafter I don't understand your example. Can you explain what would be broken?

Here I am declaring op_Addition in B the same way I would declare operator+, and the emitted IL stays the same. Why having it with operator would be any different?

https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgAQEECwAoA3nusesgMykCMAbFugPYAOA+pjDGMGPQHYA8AFQB8ACkzoAhpSh0JqAJToAvEMmUA3HgC+ePGnQAhEFjyFcJUhWQ1DDFmw5deIg2pmu5ilWs24duPQxrAHZTIhI0VHDiMwsLVwAjZXQeAFcICF84kgMAOiZWdk5ufjAeYFEEmQT5LJJ/fyA

https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgAQEECwAoA3nusesgMykCMAbFugPYAOA+pjDGMGPQHYA8AFQB8ACkzoAhpSh0JqAJToAvEMmUA3HgC+ePGnQAhEFjyFcJUhWQ1DDFmw5deIg2pmu5ilWs3mS5KlpXJgBTACcJYHow+Bc3W09lVSlfHVw9DGsAdlMif1RUPOIzCwtXACNldB4AVwgIX1KSAwA6JlZ2Tm5+MB5gUXKZcvlGkjS0oA=

My understanding from the generated IL is that unless operator+ in B is generic, the code you provided will correctly pick A.op_Addition<T> over B.operator+, because it does so over B.op_Addition already.

And if you are talking about the case when the new generic operator will hide A.op_Addition<T>, then it is not a breaking change, because that can't exist in the current code.

@gafter
Copy link
Member Author

gafter commented Dec 17, 2019

@lostmsu
Copy link

lostmsu commented Dec 17, 2019

Oh, you are actually referring to this case: https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgAQEECwAoA3nusesgMykCMAbFugPYAOA+pjDGMGPQHYAUmdAENKUOkNQBKdAF4AfMMoBuPAF88eNOgBCILHkK4SpCsho6GjAKYAnIcHo34fbYrGuJ0+YpW51uTQwzAHYDIhI0VHDiQ2NjVwAjWXQeAFcICF84km0AOiZWdk5ufgSxBMkskn9/IA= where treating operator+ as op_Addition would cause B.operator+ to be called instead of A.op_Addition without code change.

@VALLIS-NERIA
Copy link

Maybe C++ style ClassName.operator+(left, right);?

@333fred 333fred removed this from REJECTED in Language Version Planning Feb 6, 2021
@ZacharyPatten
Copy link

ZacharyPatten commented Jun 4, 2021

I just want to mention that generic operators in combination with global using static ... would allow for some really dumb shennanigans, such as the following "custom range syntax" (which already compiles):

foreach (int i in -6 -to- -4)
{
	Console.WriteLine(i);
}
foreach (long l in -2 -to- 2)
{
	Console.WriteLine(l);
}
foreach (char c in 'a'-to-'e')
{
	Console.WriteLine(c);
}
string[] names = { "Sally", "Bob", "Joe", "Eric", "Patty" };
foreach (string s in names.Where("Allen"-tᴏ-"Kevin"))
{
	Console.WriteLine(s);
}

Source code: https://gist.github.com/ZacharyPatten/3b7d7d63bc5188d3f7c863639f14765d

With generic operators the To1<T> would not need to be generic. It could just be To1 and the operator could be generic instead. Then you would only need one static to variable to work with any type.

Be careful what you wish for. :P

Another example could be measurement unit syntax. Stuff like:

  • var speedDecimal = 5m * Meters / Seconds;
  • var speedFloat = 5f * Meters / Seconds;
  • var speedInt = 5 * Meters / Seconds;

@tannergooding
Copy link
Member

The new static abstracts in interfaces feature is enabling the libraries to expose support for generic operators. The API surface and design work for that is here: dotnet/designs#205

@lostmsu
Copy link

lostmsu commented Jan 26, 2022

@tannergooding from my understanding it does not. That feature is completely orthogonal to this feature request. The new operator interfaces do not cover the scenario in the head post where non-generic class Class tries to implement operator + that accepts a generic second argument T (the exact operator here is not important):

class Class { operator+<T>(Class a, T b) => ... }

The proposed interfaces simply do not work in this case, cause you would have to put specific type in place of T here:

class Class: IAddition<Class /* TSelf */, T /* TOther */, SomeResultType>
{
  ...
}

But the problem is that T is not a single type, and class Class<T> would have a totally different semantic.

@CyrusNajmabadi
Copy link
Member

@lostmsu what's the use case such an operator would be solving?

@lostmsu
Copy link

lostmsu commented Jan 26, 2022

@CyrusNajmabadi frankly, I don't remember by now what use case I originally had for this feature. A simple example I can come up with is implementing lisp-like tuples with + concatenation operator. E.g.

var sfTuple = new Tuple<string, float>{ Head = "hi", Tail = 0xDAD };
var isfTple = 42 + sfTuple;

struct Tuple<THead, TTail> {
  THead Head;
  TTail Tail;

  public static Tuple<T, Tuple<THead, TTail>>
    operator +<T>(T value, Tuple<THead, TTail> tuple)
        => new () { Head = value, Tail = tuple };
}

But in general any construction of objects with different generic parameters than input.

The reason I dug up this issue this time was an attempt to implement the pipe trick:

using System;
using static Pipe;

int f(int v) => v + 10;

int v = pipe(10)
       .pipe(x => x * 2)
       .pipe(f);

public static class Pipe {
    public static Pipe<T> pipe<T>(T value) => new Pipe<T>(value);
}

public struct Pipe<T>  {
    public T Value{get;}
    public Pipe(T value) => Value = value;
    public static implicit operator T(Pipe<T> pipe) => pipe.Value;
    // the DSL would be much better
    // if the method below could be replaced with `operator |<TResult>`
    public Pipe<TResult> pipe<TResult>(Func<T, TResult> op) => new (op(Value));
}

@c-ohle
Copy link

c-ohle commented Aug 12, 2022

Hi, there is a simple workaround using implicit operators.
It just needs the one implementation of such an operator that works for all comparisons and more, and that's good.
But of course it's not as fast and explicit as with dedicated comparison operators.

I use this technique extensively in many variations of it in my project RationalNumerics. It saves a ton of code - unfortunately for NET 7 we have to write all conversions explicitly to comply with the new coding rules. That relativates all the good new options into it's opposite. What nonsense.

Good examples for the workaround are Float80, Float96, Float128 that all based on a template Float<T> but also more efficient performant types e.g a new BigInteger is not based on templates but uses them. The technique with the implicit operators allows for example simple interfaces like ISimpleNumber what can easely replace System.Numerics.INumber with all the complexity for the implementation to use and the run-time overhead. Based on templates, this means that type creation is done only when needed and greater flexibility for custom types, e.g. Float64 without a line of code or fast dedicated implemetation like Float128. It works all with this technique and with a dedicated full implementation we get also all the inline effects required for best perfomance.

Simple code snippet for the approach:

    struct MyTempl<T>
    {
      T data;
      public static implicit operator MyTempl<T>(T value) => new MyTempl<T> { data = value };
      public static implicit operator MyStruct(MyTempl<T> value) => value.data is int x ? x : default; // this for template 
    }

    struct MyStruct
    {
      int data;
      public static implicit operator MyStruct(int value) => new MyStruct { data = value };
      public static implicit operator MyStruct((int x, int y) value) => new MyStruct { data = value.x };  // this for tuple
      public static bool operator <(MyStruct a, MyStruct b) => a.data < b.data;
      public static bool operator >(MyStruct a, MyStruct b) => a.data > b.data;
    }

    static void test_MyStruct()
    {
      MyStruct a = 1, b = 2; 
      if (a < b) { }
      if (b > (1, 2)) { }
      if (b < (1, 2)) { }  
      MyTempl<int> c = 3;
      if (b < c) { }
    }

@Pyrdacor
Copy link

Pyrdacor commented Nov 23, 2022

@lostmsu what's the use case such an operator would be solving?

In my case I want a simple implicit conversion from MyClass<U> to MyClass<T>. This can't be done.

class MyClass<T>
{
    public static implicit operator<U> MyClass<T>(MyClass<U> x) { ... }
}

using MyClass<int> Foo;
using MyClass<uint> Bar;

Foo x = new Foo();
Bar y = x;

@c-ohle This won't work if MyStruct is generic as well.

I also want generic operators. I wonder why this isn't possible as it is for methods. The new static abstract interfaces don't change anything about this.

@soerenwolfers
Copy link

Just because I haven't seen it mentioned, this feature would allow matrix multiplication with named axes:

public static Matrix<Dim1, Dim3> operator *(Matrix<Dim1, Dim2> m1, Matrix<Dim2, Dim3> m2)

@ghost
Copy link

ghost commented Jan 23, 2023

Dear all,
If this feature is implemented, it paves the way for a beautiful syntax identical to the mathematical formulas one writes on paper or pseudocode. Linear Algebra operations of vectors and matrices and even sparse matrices become finally readable and as efficient as hand-coded expressions using for loops. Take for example:

Matrix<double> A, B;
Vector<double> a, b, c, x;
double alpha, beta, gamma;

x = gamma*c + alpha*A*b - beta*B*b;

Expression generics with generic operators will allow the syntax above avoiding operator overloading which results in temporary object (that consume a lot of memory for large array sizes > 10^7).

C# will open the door for a huge number of scientists and engineers using Fortran at the moment or C++ because such clean and clear syntax is only supported since many decades in Fortran and a few years ago in C++ with expression templates. Furthermore, complex<double> data types operations will become significantly more efficient due to avoiding temporary objects speeding up any application that involves mathematics with complex Numerics. Guys the number of applications is huge, just to give you an idea I list a few,

  1. Aeronautics
  2. Astronautics
  3. Computational Fluid Dynamics (CFD)
  4. Computational Biology
  5. Circuit Design
  6. Computer Simulations of complex physical phenomena
  7. Car industry simulations
  8. Formula F1 CFD
  9. Electrical Power Grid Simulations
  10. Computational Geophysics

these researchers currently use Fortran or C++ or languages that allow syntax closer to the mathematical expressions (Matlab, Julia), due to the complexity of the codes and formulas involved. Why not encouraging them to join our community and benefit from all this great machinery C# provides (extremely fast compilation, ease of coding, static analysis, memory safety, ...)? I would even go a step further and recommend that C# by design adopts natively such operations on its arrays double[], and matrices double[,]. Just so people who work in the areas above have nothing to lose switching to C#.

@Wasayit
Copy link

Wasayit commented Sep 3, 2023

+1 for this feature.

Generic Math is currently incomplete without a generic operator

I am expecting operator should be generic like => where T: GenericOperator
if required different types of operators like arithmetic operator, Comparision operator, so on
along with
operator overloading for generic operator will be also useful in case of same logic for multiple operators.

@claudiudc
Copy link

+1

@tannergooding
Copy link
Member

Generic Math is currently incomplete without a generic operator

I wouldn't agree it is incomplete nor that having generic operators is a "good design" for many cases.

Not only is there considerable nuance lost when talking about things like casting operators between arbitrary generic types; but there is a high amount of risk in terms of bugs and untracked behavioral issues when talking about cases like TSelf can be added with any U given a constraint.

A lot of this boils down to what is API design that goes against the Framework Design Guidelines. Therefore being things where even if they existed, it is highly unlikely that the built-in interfaces that define the core of "generic math" for .NET would ever utilize them. Of course external libraries are free to define their own types and considerations, but the general integration with the rest of .NET is likely to suffer.


For conversions, you have to consider things like:

  • on overflow is it truncating, saturating, throwing
  • is the type supported
  • if both TSelf and U implement a conversion, which should be called
    • this is basically an overload resolution issue that becomes a runtime consideration
  • etc

Then for other operators, you have to start considering things like whether an implicit conversion from U to TSelf makes sense. Something like operator +<U>(TSelf left, U right) where U : INumber<U> would be "bad", because if U was int and TSelf was byte, you get a loss of data. That could be silent, it could be throwing, and it could slip in very subtle bugs and other issues.

Not all convenient things are good. There was a deep consideration of many of these concepts and whether or not they were fundamentally "required" when the Generic math feature was designed/implemented. The cases where you may want to support doing an operation between two different types are still possible (for example, you can define a Complex<T> * T operator still), they are just limited in a way that is overall reasonable and ultimately helps keep devs away from nasty pits of failure.

That doesn't strictly mean that adding generic operators shouldn't happen either, just that features like generic math are likely not the golden ticket to making that happen as they aren't a representative use case for the majority of users working with the feature or types implementing the feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests