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

[Proposal]: Extensions #5497

Open
1 of 4 tasks
333fred opened this issue Dec 2, 2021 Discussed in #5496 · 361 comments
Open
1 of 4 tasks

[Proposal]: Extensions #5497

333fred opened this issue Dec 2, 2021 Discussed in #5496 · 361 comments
Assignees
Milestone

Comments

@333fred
Copy link
Member

333fred commented Dec 2, 2021

Discussed in #5496

Originally posted by MadsTorgersen November 30, 2021

Extensions

LDM Meetings

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-12-01.md#roles-and-extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#roles
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#roles--extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-22.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-12-11.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-02-28.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-12.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-26.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-07-22.md#extensions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-09-18.md#extensions-naming

@hez2010
Copy link

hez2010 commented Dec 2, 2021

I prefer using of or something else to distinguish the type being extended and interfaces:

public extension Foo of int : IA, IB, IC, ...
{
    ...
}

Otherwise it will be too confusing if you are extending an interface:

public extension Foo : IA, IB, IC { }

vs

public extension Foo of IA : IB, IC { }

of can be relatively safely made as a keyword since it's neither a verb nor a noun, so almost nobody would choose of as an identifier.

@HaloFour
Copy link
Contributor

HaloFour commented Dec 2, 2021

I'm curious as to how the team weighs the relative benefits between "roles" and "extension implementation". It feels that without some additional effort in the runtime the two are somewhat incompatible with each other, so if those differences can't be reconciled which of the features might the team lean towards?

Personally, I find extension implementation much more exciting than roles, but that's just my opinion.

@FaustVX
Copy link

FaustVX commented Dec 2, 2021

@hez2010
Maybe, instead of of, for would be a better name for the keyword as it's already a keyword.

public extension Foo for IA : IB, IC { }

@333fred
Copy link
Member Author

333fred commented Dec 2, 2021

I'm curious as to how the team weighs the relative benefits between "roles" and "extension implementation". It feels that without some additional effort in the runtime the two are somewhat incompatible with each other, so if those differences can't be reconciled which of the features might the team lean towards?

Who gave you an early preview of my notes? They're up now, discussion at #5500.

@sab39
Copy link

sab39 commented Dec 3, 2021

Here's a scenario that will be great fun to try to accommodate in the design:

interface IFoo { }
interface IBar { }
class Thing { }
public extension FooThing for Thing : IFoo { }
public extension BarThing for Thing : IBar { }
void Frob<T>(T t) where T : IFoo, IBar { }

Frob(new Thing());

On an unrelated bikeshedding note, what about using the existing reserved keywords explicit and implicit as modifiers, rather than treating roles and extensions as completely separate things? An extension is, more or less, just a role that gets applied implicitly based on type rather than needing to be explicitly named in the declaration. Using implicit role as the syntax would spell that correspondence out more (no pun intended) explicitly?

@CyrusNajmabadi
Copy link
Member

@sab39 Given, as you've mentioned, how similar these two concepts are. I too am looking for a good syntactic way to convey that similarity, with a clear way to do indicate in which way they differ. Thanks for the explicit/implicit idea, definitely something we'll consider!

@TahirAhmadov
Copy link

I'm not sure if I should re-post my comments from the discussion here?
In short, I think the extensions and especially interface "adapters" make sense, but roles don't. The main motivation example - DataObject - is an anti-pattern, IMO; it runs into expensive runtime changes; and causes confusion - now you have to keep in mind when looking at an identifier in a "type receiving context" if it's a type or a role.

Here's a scenario that will be great fun to try to accommodate in the design:

interface IFoo { }
interface IBar { }
class Thing { }
public extension FooThing for Thing : IFoo { }
public extension BarThing for Thing : IBar { }
void Frob<T>(T t) where T : IFoo, IBar { }

Frob(new Thing());

This is complicated, but doable using current constraints of the framework. An anonymous type can be generated:

class <mangled>Thing_IFoo_IBar : IFoo, IBar
{
  internal <mangled>Thing_IFoo_IBar(Thing thing) { this._thing = thing; }
  readonly Thing _thing;

  void IFoo.Foo() { ... } // these member(s) are copied from, or call into, FooThing
  void IBar.Bar() { ... } // these member(s) are copied from, or call into, BarThing
}
Frob(new <mangled>Thing_IFoo_IBar(new Thing()));

The same can be done for generic types, etc. Yes, it's complicated, but unlike roles, it's very possible.

@CyrusNajmabadi
Copy link
Member

The main motivation example

This was just one example. It's not the main motivation. We discussed in the LDM that there were definitely plenty of scenarios where you'd still want adapters in a strongly typed way that would be sensible.

@sab39
Copy link

sab39 commented Dec 3, 2021

@TahirAhmadov That works, more or less, for the specific example I gave, but what if Frob<T> had other constraints like where T : class or where T : struct or where T : new() or where T : SomeBaseClass? What about if it were Frob<T, T2> where T2 : T? In general it's not actually possible to generate an anonymous type that can meet all possible constraints that T would meet and also implement IFoo and IBar. This suggests to me that it's not possible to fully support this scenario without runtime assistance.

@TahirAhmadov
Copy link

The main motivation example

This was just one example. It's not the main motivation. We discussed in the LDM that there were definitely plenty of scenarios where you'd still want adapters in a strongly typed way that would be sensible.

If it's not the main motivation, surely it shouldn't be the one discussed in the OP, should it?

@CyrusNajmabadi
Copy link
Member

The OP is simply showing a demonstration. This is a broad topic and we need to spend a ton more time on it prior to even getting close to a place where we could write something up that was fully fleshed out and chock full of examples and whatnot.

@TahirAhmadov
Copy link

@TahirAhmadov That works, more or less, for the specific example I gave, but what if Frob<T> had other constraints like where T : class or where T : struct or where T : new() or where T : SomeBaseClass? What about if it were Frob<T, T2> where T2 : T? In general it's not actually possible to generate an anonymous type that can meet all possible constraints that T would meet and also implement IFoo and IBar. This suggests to me that it's not possible to fully support this scenario without runtime assistance.

The where T : class constraint simply rejects Thing extensions if Thing doesn't satisfy the constraints. The where T : new() rejects all extensions outright. where T : SomeBaseClass also rejects extensions of Thing because it's a different type. Frob<T, T2> where T2 : T is completely irrelevant.

@BhaaLseN
Copy link

BhaaLseN commented Dec 3, 2021

Back with .NET Framework, I've often ran into situations where i wanted a Math.Clamp<T>(T value, T min, T max), Math.Max(TimeSpan val1, TimeSpan val2) or Path.GetRelativePath(...) for discoverability; but there was no way for me to get this done. Same with string.Contains(string, StringComparison) etc. except as instance extension (which is somewhat taken care of by extension methods though.)
Nowadays most are in there since either .NET Core or .NET 5/6, but it would feel more natural to simply extend the existing classes (with a very high-up namespace inside the project, so its most likely in scope all the time; or with a global using for example) when another one of those situations comes up. At least compare to MathEx, MathUtilities, PathHelpers etc. which often aren't as obvious.

The only thing I don't quite get is why we need two keywords here, role and extension. They mean different things if you talk about them, sure, but does this actually matter once you write the code? I'd assume they'll be lowered to virtually the same thing during compilation after all, and I can practically hear the "what's the difference" question coming when I present this to my team during a knowledge transfer meeting.

@TahirAhmadov
Copy link

TahirAhmadov commented Dec 3, 2021

The OP is simply showing a demonstration. This is a broad topic and we need to spend a ton more time on it prior to even getting close to a place where we could write something up that was fully fleshed out and chock full of examples and whatnot.

That's the thing, it would be very interesting to see an example which would demonstrate how roles make something worthwhile possible or significantly easier, before effort is spent on prototypes, implementation planning, etc.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Dec 3, 2021

That's fine. It's something we're working on at this moment '-). The point was raised and was something we intend to get to and write more on. I def don't want us to get the impression that it's just for that. Thanks!

@orthoxerox
Copy link

Roles feel like they need a validator method, something that is invoked to by the "implicit conversion" to ensure that the underlying object can fill in that role. I'm not even sure the conversion should be implicit. I'm sure it will be annoying to do stuff like public Customer Customer => (Customer)this["Customer"]; over and over again, but I also want to be able to say if (payload is Order order) { ....

@sab39
Copy link

sab39 commented Dec 3, 2021

Hmm, that almost makes it sound like you want Extension DUs...

@orthoxerox
Copy link

I don't want them to be a DU per se, it's more similar to getting an object from some API and casting it to the expected type. Right now the roles work more like dynamic instead.

@vladd
Copy link

vladd commented Dec 3, 2021

@orthoxerox F# has a feature Partial Active Patterns which looks somewhat like your idea.

@hez2010
Copy link

hez2010 commented Dec 4, 2021

This is complicated, but doable using current constraints of the framework. An anonymous type can be generated:

class <mangled>Thing_IFoo_IBar : IFoo, IBar
{
  internal <mangled>Thing_IFoo_IBar(Thing thing) { this._thing = thing; }
  readonly Thing _thing;

  void IFoo.Foo() { ... } // these member(s) are copied from, or call into, FooThing
  void IBar.Bar() { ... } // these member(s) are copied from, or call into, BarThing
}
Frob(new <mangled>Thing_IFoo_IBar(new Thing()));

The same can be done for generic types, etc. Yes, it's complicated, but unlike roles, it's very possible.

C# isn't the only language on CoreCLR, without runtime support how would you expect roles to be defined and used in other languages? Other languages don't recognize the mangled anonymous class.

@TahirAhmadov
Copy link

C# isn't the only language on CoreCLR, without runtime support how would you expect roles to be defined and used in other languages? Other languages don't recognize the mangled anonymous class.

The pseudocode I wrote was specifically for extensions, not roles.
In any case, though, the mangled anonymous type is generated at the call site, not at the extension declaration site. Specifically because there can be multiple permutations of extensions (or roles - ignoring the fact that I don't like the idea of roles/shapes), these machinations have to be performed when all the information is available: what interfaces are "adapted", etc.
Also, regardless of the language, the extension will have to be added somehow to the metadata; the easiest way would be using a class with some special attributes. The other languages can decide whether to implement this feature - in which case they can interpret these attributes like C# does; otherwise, it becomes a class, probably a static one, which they can use in an "old school way". The same is true for existing extension methods.
Further, even when it's a simple scenario, for interface "adaptation" to work, the easiest way is again, an anonymous type:

class Thing { }
interface IFoo { void Foo(); }
extension FooThing: Thing, IFoo { void Foo() { ... } }
void Frob(IFoo foo) { }
// this line:
Frob(new Thing());
// is compiled to this:
class <mangled>Thing_IFoo : IFoo
{
  internal <mangled>Thing_IFoo(Thing thing) { this._thing = thing; }
  readonly Thing _thing;

  void IFoo.Foo() { ... } // these member(s) are copied from, or call into, FooThing
}
Frob(new <mangled>Thing_IFoo(new Thing()));

@Thaina
Copy link

Thaina commented Dec 4, 2021

I also want to voice that I wish there would be some keyword being reused instead of casting new keyword role and extension. Or at least create only one and use implicit/explicit as above

Or implicit class possible?

Aside from that I have nothing against, and fully support this issue

@hez2010
Copy link

hez2010 commented Dec 4, 2021

instead of casting new keyword role and extension. Or at least create only one and use implicit/explicit as above

Or implicit class possible?

Keywords can be introduced as contextual keywords so it can be made not to introduce breaking changes.

@Thaina
Copy link

Thaina commented Dec 4, 2021

@hez2010 I know there is no breaking change but it still should be the last option to introduce any new keyword. If there would be any possible for composite or reuse then we should

@333fred
Copy link
Member Author

333fred commented Dec 4, 2021

I found the idea of implicit and explicit very interesting and forwarded your comment to our working group @sab39, thanks for the suggestion!

@iam3yal
Copy link
Contributor

iam3yal commented Dec 4, 2021

I don't get why roles need an implicit and explicit modifiers, can't they be applied based on the context? what does it mean to have these modifiers? why treat them more or less the same and not exactly the same where the only difference is context? I get the you're trying out different ideas but merge these concepts needs to be core principle the way I think about is similar to aggregation vs composition where aggregation is an extension of existing type and composition is a wrapper the only difference is what they user want them to be based on context and not how they were constructed I don't think we want to end up with a situation where "I can do this when it's explicit but not when it's implicit or vice-versa" but maybe I'm misunderstanding why we need to have different rules for these two concepts, it's not clear whether they are similar or identical features yet but based on the OP I think they are either identical or similar to the point where it can be confusing to grasp why we are speaking about two different concepts.

I'll just copy/paste my comment from the other post so something like this:

// Customer.cs
namespace Data;

public extension Customer : DataObject // Wrapper type
{
    public string Name => this["Name"].AsString();
    public string Address => this["Address"].AsString();
    public IEnumerable<Order> Orders => this["Orders"].AsEnumerable();
}

// JsonDataObject.cs
namespace Data;

using JsonLibrary;

public extension JsonDataObject : DataObject // Extension type
{
    public string ToJson() { … this … }
    public static T FromJson<T>(string json) {}
}

// Program.cs / Main method
using Data;
using Data.JsonDataObject; // Importing a specific extension type
using Data.*; // Importing all extensions types in the namespace Data

var customer = customer.FromJson<Customer>(args[0]);
WriteLine(customer.ToJson());

@Reinms
Copy link

Reinms commented Dec 4, 2021

Would this be allowed under roles?

role Foo<T> : T
    where T : ISomeInterface
{
}

or would we be forced to directly extend the interface and bring in boxing conversions all over the place as we implicitly cast back and forth in a generic function?

@TahirAhmadov
Copy link

TahirAhmadov commented Dec 4, 2021

Thinking about it, I imagine this happening:

class Thing { }
interface IFoo { void Foo(); }
// the following line
public extension FooThing: Thing, IFoo { void Foo() { ... } }
// is compiled to:
// these attributes are once per assembly, similar to NRT attributes
class ExtensionTypeAttribute { public ExtensionTypeAttribute(params Type[] types) { ... } ... }
class ExtensionInstanceMemberAttribute {  }
class ExtensionStaticMemberAttribute {  }
// the actual extension becomes:
[ExtensionType(typeof(Thing), typeof(IFoo))]
public static class FooThing
{
  [ExtensionInstanceMember]
  public static void Foo(Thing @this) { ... }
}

void Frob(IFoo foo) { }

// this line:
Frob(new Thing());
// is compiled to this:
class <mangled>Thing_IFoo : IFoo
{
  internal <mangled>Thing_IFoo(Thing thing) { this._thing = thing; }
  readonly Thing _thing;

  void IFoo.Foo() { FooThing.Foo(this._thing); }
}
Frob(new <mangled>Thing_IFoo(new Thing()));

@TahirAhmadov
Copy link

Sorry to pester - are there any decisions about which member types will make it in C# 13?

@CyrusNajmabadi
Copy link
Member

Everything is currently in flux :-)

@hez2010
Copy link

hez2010 commented Jun 7, 2024

I still think abandoning identity preservation as a goal is the way to go.

It's not how type classes work. Identity preservation is pretty important when it involves interface implementation, otherwise, any implicit extension coming into the scope can simply break your existing code by accident.

While it's too early to talk about it so I am not able to speak more about it, there are already some initial thoughts about this feature which adds a lookup table to the metadata and looks up it at runtime to make it work.

@TahirAhmadov
Copy link

@hez2010 how so? Please elaborate on the "break your existing code by accident" part.

@hez2010
Copy link

hez2010 commented Jun 7, 2024

@hez2010 how so? Please elaborate on the "break your existing code by accident" part.

Let's say you have a dictionary for value cache per type Dictionary<Type, object?>, it stores the latest value of given type in the dictionary:

Dictionary<Type, object> dict;
bool TryGetValue(Type key, out object result) => ...;

Now you may have a method UpdateValue:

void UpdateValue<T>(T value) where T : IFoo
{
    dict[value.GetType()] = value;
    // or even dict[typeof(T)] = value;
}

If the type identity cannot be preserved, let's say now you have an implicit extension Ext which implements IFoo for int and it comes into the scope, then the below code will simply break as it ends up updating the value for Ext, not for int, and results in bad behavior:

UpdateValue(42);
TryGetValue(typeof(int), out var result); // it will return false instead of true!

And you don't have any way to "opt-out" this extension if the using is imported by global using from some 3rd packages.
The above pattern is very common in real world apps.

Another typical use case can be CsWinRT where one can extend an IWinRTMarshallingDescriptor<T> for all primitive types, built-in marshalled types, and all projected types. This will allow us to decouple the marshalling code from the projected types.

interface IWinRTMarshallingDescriptor<T, TAbi>
    where T : IWinRTMarshallingDescriptor<T, TAbi>
{
    static abstract T? FromAbi(TAbi value);
    static abstract TAbi FromManaged(T? value);
    // ...
}

If the type identity cannot be preserved, the above code will result in undefined behavior.

@TahirAhmadov
Copy link

TahirAhmadov commented Jun 7, 2024

@hez2010 The UpdateValue(42); had been invalid code before the implicit extension came into scope, so this isn't a breaking change.

If there was another overload, void UpdateValue(int x) { }, or void UpdateValue(object x) { }, it then depends on overload resolution. It would still pick int overload, but would switch from object to T overload, yes. But I would argue even in that case, the developer still took action to cause the change in behavior, which means it's not a silent breaking change.

All this leaving aside the fact that this is a very contrived scenario.

PS. I haven't done anything with WinRT so I will not comment on that.

PPS. My main argument here is that if you have a method which expects IFoo, it should use IFoo. If the method cares about the identity, it shouldn't expect IFoo. It should be:

void UpdateValue(object value)
{
    dict[value.GetType()] = value;
}

@hez2010
Copy link

hez2010 commented Jun 7, 2024

@hez2010 The UpdateValue(42); was invalid code before the implicit extension came into scope, so this isn't a breaking change.

That's not correct. The extension may come from assembly A, and the UpdateValue may come from assembly B. Now you are on assembly C which references both A and B, you won't even be aware about this while using UpdateValue. What's more, if the value was boxed into IFoo it can lead more problems if the type identity lost, for example, x.Equals(42) will simply return false as Ext is not int, where x is returned from the cache and boxed into IFoo.

Type identity preservation is not necessary in extensions without interface implementation, but an extension implementing interfaces for existing type will become type classes, where type identity does matter.

@TahirAhmadov
Copy link

That's not correct. The extension may come from assembly A, and the UpdateValue may come from assembly B. Now you are on assembly C which references both A and B, you won't even be aware about this while using UpdateValue.

It doesn't matter what assembly each part of this code is in. UpdateValue(42) would have never bound to void UpdateValue<T>(T value) where T : IFoo before the extension came into scope. SharpLab

What's more, if the value was boxed into IFoo it can lead more problems if the type identity lost, for example, x.Equals(42) where x is returned from the cache will simply return false as Ext is not int.

Equals is not "identity driven" - I'm totally for overriding the virtual Equals method as needed to allow that equality to succeed.

@HaloFour
Copy link
Contributor

HaloFour commented Jun 7, 2024

I think that this is a good conversation for a separate discussion, or for Discord, given extension implementation is not planned for C# 13.0 and the design is still completely up in the air. As they are considered to be type classes, it feels worthwhile to consider how type classes are used and are expected to behave in other languages.

@hez2010
Copy link

hez2010 commented Jun 7, 2024

Yeah. Extensions implementing interfaces is type classes which is a different feature from extensions, and here it only shares the syntax with extensions. People will start to rely on extensions implementing interfaces for dynamic ad-hoc polymorphism, where type identity preservation becomes important. The feature is out-of-scope for C# 13.0, we may need to see how type classes work in other languages before talking about the implementation. I believe metadata and some runtime/reflection APIs will also need to be updated to support it though.
There's some details on this feature: https://github.com/dotnet/csharplang/blob/extension-interfaces/proposals/extension-interfaces.md

@ds5678
Copy link

ds5678 commented Jun 19, 2024

The extension_underlying_type type may not be dynamic, a pointer, a ref struct type, a ref type or an extension.

Is there an implementation reason for why pointers, ref structs, and ref types can't be used as the underlying type? With the team pursuing a static method approach to emitting extensions, this restriction appears arbitrary.

@colejohnson66
Copy link

@ds5678 I'd have to assume restrictions are due to a "get it working now, and iron out the details later" approach.

@jrmoreno1
Copy link

@ds5678 : what would an extension on a pointer do?

@ds5678
Copy link

ds5678 commented Jun 20, 2024

In interop scenarios, pointers are often used as typed references to native objects.

@aetos382
Copy link

aetos382 commented Jul 1, 2024

Will extensions (not just interface additions, any part of them) not be shipped with C# 13?

@glen-84
Copy link

glen-84 commented Aug 12, 2024

I know that suggestions for extensions for enums have been proposed for different use cases, but have there been any thoughts on how extensions might add enum members to an existing enum? There are a few proposals on this repo looking to be able to "extend" existing enums with new values:

// lots of members omitted for brevity
public enum HttpStatus {
    OK = 200,
    BadRequest = 400,
    NotFound = 404
}

public explicit extension OtherHttpStatus for HttpStatus {
    // no idea how this syntax would work, this is just illustrative
    Teapot = 418
}

Coming back to this, if the enum members were not initialized, and the enum was extended more than once (in more than one file), how would the values be assigned?

Perhaps both the extended and extending type should be required to specify constant values, so that the values are deterministic?

@hamarb123
Copy link

I know that suggestions for extensions for enums have been proposed for different use cases, but have there been any thoughts on how extensions might add enum members to an existing enum? There are a few proposals on this repo looking to be able to "extend" existing enums with new values:

I mean, you could almost certainly write this, even without explicit support for adding new enum members (whether you should is another question):

public implicit extension ConsoleColorExtensions for ConsoleColor
{
    public const ConsoleColor NewColor = (ConsoleColor)71;
}

@zms9110750
Copy link

I have a problem.
Game engines provide a vector class and basic mathematical methods.
If I am dissatisfied with them, I may write extensions. But if I want to apply to all vectors provided by engines, is there a way?

To avoid the extension of vector 2D affecting vector 3D, we should specify the type of extension to be applied.
But specify without modifying the code.
Declare ( extended of extension ) for bridging in a specific project?

@alrz
Copy link
Contributor

alrz commented Aug 25, 2024

This is probably further down the line but I think this could solve record custom equality for lists,

public implicit extension EnumerableEquitableExtension<T> : IEquitable<IEnumerable<T>> for IEnumerable<T> 
    where T : IEquitable<T> 
{
    public bool Equals(IEnumerable<T> other) => this.SequenceEqual(other);
}

That's assuming EqualityComparer<T>.Default could take extensions into account.

@HaloFour
Copy link
Contributor

@alrz

That's assuming EqualityComparer<T>.Default could take extensions into account.

I don't imagine that it could? Given that property is already compiled there's nowhere for the extension binding to occur. You'd need some kind of runtime registry of extensions (or implementations) per type, I'd think. However, if the goal was specifically to support that for equality comparison of the fields of a record, the language could have always supported mechanisms to provide custom equality comparers, it's just something that the language team intentionally didn't want to do. I don't see this as a way to backdoor that decision, given I think it'd be a much messier approach.

@alrz
Copy link
Contributor

alrz commented Aug 26, 2024

I don't imagine that it could?

I think that's a question of its own. If these interfaces are supported by is and IsAssignableFrom, EqualityComparer support would fall out of that.

@HaloFour
Copy link
Contributor

@alrz

I think that's a question of its own. If these interfaces are supported by is and IsAssignableFrom, EqualityComparer support would fall out of that.

I doubt it. EqualityComparer<List<Foo>>.Default wouldn't know anything about the potential extensions that exist for List<Foo> that happen to be in scope at the point of invocation. You would need the compiler to understand the specific context and emit said code, which could be a new strategy that the compiler could employ to support custom equality/sorting for types. Personally I think that'd be way too subtle a way to implement that.

@colejohnson66
Copy link

Agreed. If you want a custom IEquatable extension as shown, you'd need to do EnumerableEquitableExtension<List<Foo>>

@alrz
Copy link
Contributor

alrz commented Aug 26, 2024

EqualityComparer<List<Foo>>.Default wouldn't know anything about the potential extensions

My point was that this is not about EqualityComparer specifically. Extending is and IsAssignableFrom is certainly not impossible (new conversions can be encoded using attributes for both compiler and reflection to use), but it is a question if these implementations are going to be integrated into the type system and if so to what degree? - the minimum being only in generic constraints, maybe.

@HaloFour
Copy link
Contributor

Extending is and IsAssignableFrom is certainly not impossible (new conversions can be encoded using attributes for both compiler and reflection to use), but it is a question if these implementations are going to be integrated into the type system and if so to what degree?

I totally agree, it is an interesting conversation. The real question is can it be done efficiently without effectively making all type checks that much more expensive. I assume it wouldn't affect already compiled code, but maybe the compiler could emit additional type checks for extensions that are in scope?

// given
public interface IFoo { }
public extension Int32FooExtension for int : IFoo { }

object o = 123;

// then
if (o is IFoo foo) { }

// lowers to
if (o is IFoo foo) {
    // already is IFoo
}
// otherwise enumerate extensions in scope that implement IFoo
if (o is int $temp) {
    IFoo foo = new Int32FooExtension($temp);
}

@alrz
Copy link
Contributor

alrz commented Aug 26, 2024

In Rust there's a whole concept of trait objects to support this by capturing anything about the trait impl at the time of assignment. Generic contraints are different because the information is available statically so naturally there's no performance penalty if used that way, otherwise you get a complete different codegen.

@CyrusNajmabadi
Copy link
Member

so naturally there's no performance penalty if used that way

Well, there can be a penalty. It just depends on if it's ok where that penalty happens. In rust, this penalty often happens at initial compile time, as well as with potentially huge code-size explosion. That's often the right tradeoff for users, but it's not universally so. A lot of people have a distaste about runtime costs. But practically speaking, it's usually totally ok, and extremely high perf code doesn't care anyways, since it's not using tehse operations to begin with as even the stock behavior today is too costly for them.

@ds5678
Copy link

ds5678 commented Aug 26, 2024

I'm against implicit extensions affecting EqualityComparer.Default. That feels like spooky action at a distance. Only explicit extensions or extensions used as type parameters should affect it.

Okay:

EqualityComparer<ExplicitExtension>.Default
EqualityComparer<ImplicitExtension>.Default
EqualityComparer<T>.Default // where an extension is used as the type argument 

Not okay:

EqualityComparer<BaseType>.Default

@HaloFour
Copy link
Contributor

@alrz

In Rust there's a whole concept of trait objects to support this by capturing anything about the trait impl at the time of assignment. Generic contraints are different because the information is available statically so naturally there's no performance penalty if used that way, otherwise you get a complete different codegen.

I don't know a whole lot about Rust but reading a bit about implementation and coherence it seems Rust also forbids the ability to provide an implementation for an external trait to an external type. So this kind of behavior wouldn't be permitted in Rust anyway, and that is intentional so that you cannot change the meaning of existing code. It also prevents the problem of having multiple implementations for the same trait to the same type. That's a very different design from extensions where the point of extension implementations would be to extend external types, and extensions can be scoped by namespace.

The idea of extensions affecting the runtime behavior of code is certainly an interesting conversation to have, but I think there are several cans of worms there.

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