csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Generic operators support in C#.

Open gafter opened this issue 6 years ago • 26 comments

@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 avatar Aug 11 '17 00:08 gafter

I wonder if this would still be needed if we did type classes (shapes) See https://github.com/dotnet/csharplang/issues/110 and https://github.com/dotnet/csharplang/issues/164

gafter avatar Aug 11 '17 00:08 gafter

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.

Richiban avatar Sep 20 '17 09:09 Richiban

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

IanWold avatar Sep 20 '17 12:09 IanWold

See also #108, #612

orthoxerox avatar Sep 22 '17 08:09 orthoxerox

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.

lostmsu avatar Sep 15 '19 06:09 lostmsu

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.

gafter avatar Dec 16 '19 20:12 gafter

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

lostmsu avatar Dec 17 '19 07:12 lostmsu

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?

munael avatar Dec 17 '19 16:12 munael

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.

333fred avatar Dec 17 '19 16:12 333fred

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

"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.

gafter avatar Dec 17 '19 20:12 gafter

@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.

lostmsu avatar Dec 17 '19 23:12 lostmsu

If invoking something called op_Addition would invoke something declared operator +, then the program at https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgAQEECwAoA3nusesgMykCMAbFugPYAOA+pjDGMGPQHYA8AFQB8ACkzoAhpSh0JqAJToAvEMmUA3HgC+ePGnQAhEFjyFcJUhWQ1DDRgFMAThOD1H8EQbUyvcxSrVNXB1cPQxrAHZTIhI0VBjiMwsLLwAjZXQeAFcICCDkkgMAOiZWdk5ufjAeYFFUmVT5fJIQkKA= would change behavior.

gafter avatar Dec 17 '19 23:12 gafter

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.

lostmsu avatar Dec 17 '19 23:12 lostmsu

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

VALLIS-NERIA avatar Nov 26 '20 12:11 VALLIS-NERIA

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;

ZacharyPatten avatar Jun 04 '21 16:06 ZacharyPatten

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: https://github.com/dotnet/designs/pull/205

tannergooding avatar Jun 04 '21 17:06 tannergooding

@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.

lostmsu avatar Jan 26 '22 05:01 lostmsu

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

CyrusNajmabadi avatar Jan 26 '22 11:01 CyrusNajmabadi

@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));
}

lostmsu avatar Jan 26 '22 18:01 lostmsu

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) { }
    }

c-ohle avatar Aug 12 '22 19:08 c-ohle