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

The design space for extensions #8548

Open
MadsTorgersen opened this issue Oct 30, 2024 · 110 comments
Open

The design space for extensions #8548

MadsTorgersen opened this issue Oct 30, 2024 · 110 comments
Assignees

Comments

@MadsTorgersen
Copy link
Contributor

MadsTorgersen commented Oct 30, 2024

The design space for extensions

Let's put the current proposals for extensions in a broader context, and compare their pros and cons.

Background: How we got here

The competing approaches and philosophies around how to express the declaration of extension members trace their roots all the way back to when extension methods were first designed.

C# 3: The start of extension methods

C# 3 shipped with the extension methods we know today. But their design was not a given: An alternative proposal was on the table which organized extension methods in type-like declarations, each for one specific extended (underlying) type. In this model, extension method declarations would look like instance method declarations, and future addition of other member kinds - even interfaces - would be syntactically straightforward. I cannot find direct references to this first type-based proposal, but here is a slightly later version from 2009 that was inspired by it:

public extension Complex : Point
{
    public Point Scale(double s) { return new Point(X * s, Y * s); }
    public double Size { get { return Math.Sqrt(X*X + Y*Y); } }
    public static Point operator +(Point p1, Point p2) { return new Point(p1.X + p2.X, p1.Y + p2.Y); }
}

Ultimately the current design was chosen for several reasons. Importantly it was much simpler: a syntactic hack on top of static methods. C# 3 was already brimming with heavy-duty features - lambdas, expression trees, query expressions, advanced type inference, etc. - so the appetite to go big on extension methods was limited. Moreover, the static method approach came with its own disambiguation mechanism - just call as a static method! - and allowed convenient grouping of extension methods within one static class declaration. The extension methods of System.Linq.Enumerable would have needed to be spread across about 15 extension type declarations if they had been split by underlying type.

But perhaps most significantly, we didn't know extension methods were going to be such a hit. There was a lot of skepticism in the community, especially around the risks of someone else being able to add members to your type. The full usefulness of the paradigm was not obvious even to us at the time; mostly we needed them for the query scenario to come together elegantly. So betting on them as a full-fledged new feature direction felt like a risky choice. Better to keep them a cheap hack to start with.

C# 4: Foundered attempts at extension members

Of course extension methods were a huge success in their own right, and the community was immediately asking for more; especially extension properties and extension interfaces. The LDM went to work on trying to generalize to all member kinds, but felt captive to the choices made in C# 3. We felt extension members would have to be a continuation, not just philosophically but syntactically, of the extension methods we'd shipped. For instance, extension properties would have to either use property syntax and take an extra this parameter somehow, or we'd need to operate at the lowered level of set and get methods representing the accessors of properties. Here is an example from 2008:

public extension E
{
    public static string Name<T>(this Foo<T> myself){ get {} set {} }
    public static V this<K,V>(this Dict<K,V> dict)[K index] { get {} }
    public static event Handler<MyArgs> OnExplode<T>(this Foo<T> it) { 
    	add {}
    	remove {}
    }
    public static operator + (BigInteger i, Complex c) {}
    public static implicit operator Complex(BigInteger i) {}
}

These explorations led to proposals of unbearable complexity, and after much design and implementation effort they were abandoned. At the time we were not ready to consider rebooting extensions with an alternative syntax, one that would leave the popular classic extension methods behind as a sort of legacy syntax.

The return of type-based extensions

The Haskell programming language has type classes, which describe the relationships within groups of types and functions, and which, crucially, can be applied after the fact, without those types and functions participating. A proposal from Microsoft Research in Cambridge for adding type classes to C# triggered a string of proposals that eventually led back to extension interfaces: If extension members could somehow help a type implement an interface without the involvement of that type, this would facilitate similar adaptation capabilities to what type classes provide in Haskell, and would greatly aid software composition.

Extension interfaces fit well with the old alternative idea that extensions were a form of type declaration, so much so that we ended up with a grand plan where extensions were types, and where such types would even be a first class feature of their own - separate from the automatic extension of underlying types - in the form of roles.

This approach ran into several consecutive setbacks: We couldn't find a reasonable way to represent interface-implementing extensions in the runtime. Then the implementation of the "typeness" of extensions proved prohibitively expensive. In the end, the proposal had to be pared back to something much like the old alternative design from above: extensions as type declarations, but with no "typeness" and no roles. Here's a recent 2024 example:

extension E for C
{
    // Instance members
    public string P { get => f; set => f = value.Trim(); }         // Property
    public T M<T>() where T : IParsable<T> => T.Parse(f, default); // Method
    public char this[int index] => f[index];                       // Indexer
    public C(string f) => this.f = f;                              // Constructor

    // Static members
    public static int ff = 0;                                // Static field
    public static int PP { get; set => field = Abs(value); } // Static property
    public static C MM(string s) => new C(s);                // Static method
    public static C operator +(C c1, C c2) => c1.f + c2.f;   // Operator
    public static implicit operator C(string s) => new C(s); // UD conversion

}

We will refer to the resulting flavor of design as "type-based extensions", because the underlying type of the extension is specified on the extension type itself, and the members are just "normal" instance and static member declarations, including providing access to the underlying value with the this keyword rather than a parameter.

The return of member-based extensions

Now that the bigger story of extensions as types with interfaces has been put on hold with its future prospects in question, it is worth asking: Are we still on the right syntactic and philosophical path? Perhaps we should instead do something that is more of a continuation of classic extension methods, and is capable of bringing those along in a compatible way.

This has led to several proposals that we will collectively refer to as "member-based extensions". Unlike most of the abandoned C# 4 designs of yore, these designs do break with classic extension methods syntactically. Like the type-based approach they embrace an extension member declaration syntax that is based on the corresponding instance member declaration syntax from classes and structs. However, unlike type-based extensions, the underlying type is expressed at the member level, using new syntax that retains more characteristics of a parameter.

Here are a few examples from this recent proposal:

public partial extensions Extensions
{
    public SourceTextContainer (ITextBuffer buffer).AsTextContainer()
        => TextBufferContainer.From(buffer);

    internal TextLine (ITextSnapshotLine line).AsTextLine()
        => line.Snapshot.AsText().Lines[line.LineNumber];
}
internal extensions IComparerExtensions<T> for IComparer<T>
{
    public IComparer<T> comparer.Inverse => new InverseComparer<T>(comparer)
}

The motivation is not just a closer philosophical relationship with classic extension methods: It is an explicit goal that existing classic extension methods can be ported to the new syntax in such a way that they remain source and binary compatible. This includes allowing them to be called as static methods, when their declarations follow a certain pattern.

We've had much less time to explore this approach. There are many possible syntactic directions, and we are just now beginning to tease out which properties are inherent to the approach, and which are the result of specific syntax choices. Which leads us to the following section, trying to compare and contrast the two approaches.

Comparing type-based and member-based proposals

Both approaches agree on a number of important points, even as the underlying philosophy differs in what currently feels like fundamental ways:

  • Member syntax: In both approaches the member declaration syntax is based on the corresponding instance member declaration syntax. They may be adorned or modified in different ways, but neither attempts to use the naked static method syntax of classic extension methods, or otherwise embrace the lowered form in declaration syntax.
  • Type syntax: Both also introduce a new form of type declaration (with the keyword extension or extensions) to hold extension member declarations. Neither approach keeps extension members in static classes.
  • Abstraction: Both generally hide the low-level representation of the declaration from language-level use (with one exception in the member-based approach for compatibility purposes). This means that both have the same need for a disambiguation mechanism to replace classic extension methods' ability to be called directly as static methods.
  • Lookup: Both are amenable to pretty much the same range of design options regarding extension member lookup, inference of type arguments, and overload resolution.

And of course both approaches share the same overarching goal: to be able to facilitate extension members of nearly every member kind, not just instance methods. Either now or in the future this may include instance and static methods, properties, indexers, events, operators, constructors, user-defined conversions, and even static fields. The only exception is members that add instance state, such as instance fields, auto-properties and field-like events.

The similarities make it tempting to search for a middle ground, but we haven't found satisfactory compromise proposals (though not for lack of trying). Most likely this is because the differences are pretty fundamental. So let's look at what divides the two approaches.

Relationship to classic extension methods

The core differentiating factor between the two approaches is how they relate to classic extension methods.

In the member-based approach, it is a key goal that existing classic extension methods be able to migrate to the new syntax with 100% source and binary compatibility. This includes being able to continue to call them directly as static methods, even though they are no longer directly declared as such. A lot of design choices for the feature flow from there: The underlying type is specified in the style of a parameter, including parameter name and potential ref-kinds. The body refers to the underlying value through the parameter name.

Only instance extension methods declared within a non-generic extensions declaration are compatible and can be called as static methods, and the signature of that static method is no longer self-evident in the declaration syntax.

The type-based approach also aims for comparable expressiveness to classic extension methods, but without the goal of bringing them forward compatibly. Instead it has a different key objective, which is to declare extension members with the same syntax as the instance and static members they "pretend" to be, leaving the specification of the underlying type to the enclosing type declaration. This "thicker" abstraction cannot compatibly represent existing classic extension methods. People who want their existing extension methods to stay fully compatible can instead leave them as they are, and they will play well with new extension members.

While the type-based approach looks like any other class or struct declaration, this may be deceptive and lead to surprises when things don't work the same way.

The member-based approach is arguably more contiguous with classic extension methods, whereas the type-based approach is arguably simpler. Which has more weight?

Handling type parameters

An area where the member-based approach runs into complexity is when the underlying type is an open generic type. We know from existing extension methods that this is quite frequent, not least in the core .NET libraries where about 30% of extension methods have an open generic underlying type. This includes nearly all extension methods in System.Linq.Enumerable and System.MemoryExtensions.

Classic extension methods facilitate this through one or (occasionally) more type parameters on the static method that occur in the this parameter's type:

public static class MemoryExtensions
{
    public static Span<T> AsSpan<T>(this T[]? array);
}

The same approach can be used to - compatibly - declare such a method with the member-based approach:

public extensions MemoryExtensions
{
    public Span<T> (T[]? array).AsSpan<T>();
}

We should assume that open generic underlying types would be similarly frequent for other extension member kinds, such as properties and operators. However, those kinds of member declarations don't come with the ability to declare type parameters. If we were to declare AsSpan as a property, where to declare the T?

This is a non-issue for the type-based approach, which always has type parameters and underlying type on the enclosing extension type declaration.

For the member-based approach there seem to be two options:

  1. Allow non-method extension members to also have type parameters.
  2. Allow the type parameter and the specification of the underlying type on the enclosing type.

Both lead to significant complication:

Type parameters on non-method extension members

Syntactically we can probably find a place to put type parameters on each kind of member. But other questions abound: Should these be allowed on non-extension members too? If so, how does that work, and if not, why not? How are type arguments explicitly passed to each member kind when they can't be inferred - or are they always inferred?

public extensions MemoryExtensions 
{
    public Span<T> (T[]? array).AsSpan<T> { get; }
}

This seems like a big language extension to bite off, especially since type parameters on other members isn't really a goal, and current proposals don't go there.

Allow type parameters and underlying type to be specified on the enclosing type declaration

If the enclosing extensions type declaration can specify type parameters and underlying type, that would give members such as properties a place to put an open generic underlying type without themselves having type parameters:

public extensions MemoryExtensions<T> for T[]?
{
    public Span<T> (array).AsSpan { get; }
}

This is indeed how current member-based proposals address the situation. However, this raises its own set of complexities:

  1. Classic extension methods are purely method-based, and the enclosing static class is just a plain container contributing nothing but its name. Here, though, the enclosing extensions declaration starts carrying crucial information for at least some scenarios.
  2. There are now two distinct ways of providing the underlying type for an extension member, and figuring out which to use becomes a bit of a decoder-ring situation:
    • For a compatible port of a classic extension method, the underlying type and any type parameters must be on the member.
    • For non-method extension members with an open generic underlying type, the underlying type and the type parameters must be on the enclosing type.
    • For extension methods that do not need to be compatible and have an open generic underlying type, the underlying type and the type parameters can be specified either on the method or the type, but the generated code will be incompatible between the two.
    • For extension members with a closed underlying type, the underlying type can be defined either on the method or the type, and the two are interchangeable.
  3. Once an extensions declaration specifies an underlying type, it can no longer be shared between extension members with different underlying types. The grouping of extension members with different underlying types that is one of the benefits of the member-based approach doesn't actually work when non-method extension members with open generic underlying types are involved: You need separate extensions declarations with separate type-level underlying types just as in the type-based approach!
public extensions ArrayMemoryExtensions<T> for T[]?
{
    public Span<T> (array).AsSpan { get; }
}
public extensions ArraySegmentMemoryExtensions<T> for ArraySegment<T>?
{
    public Span<T> (segment).AsSpan { get; }
}

In summary, classic extension methods rely critically on static methods being able to specify both parameters and type parameters. A member-based approach must either extend that capability fully to other member kinds, or it must partially embrace a type-based approach.

Tweaking parameter semantics

An area where the type-based approach runs into complexity is when the default behavior for how the underlying value is referenced does not suffice, and the syntax suffers from not having the expressiveness of "parameter syntax" for the underlying value.

This is a non-issue for the member-based approach, which allows all this detail to be specified on each member.

There are several kinds of information one might want to specify on the underlying value:

By-ref or by_value for underlying value types

In classic extension methods, the fact that the this parameter is a parameter can be used to specify details about it that real instance methods don't get to specify about how this works in their body. By default, this parameters, like all parameters, are passed by value. However, if the underlying type is a value type they can also be specified as ref, ref readonly and in. The benefit is to avoid copying of large structs and - in the case of ref - to enable mutation of the receiver itself rather than a copy.

The use of this varies wildly, but is sometimes very high. Measuring across a few different libraries, the percentage of existing extension methods on value types that take the underlying value by reference ranges from 2% to 78%!

The type-based approach abstracts away the parameter passing semantics of the underlying value - extension instance members just reference it using this, in analogy with instance members in classes and structs. But classes and structs have different "parameter passing" semantics for this! In classes this is by-value, and in structs this is by ref - or ref readonly when the member or struct is declared readonly.

There are two reasonable designs for what the default should be for extension members:

  1. Follow classes and structs: When the underlying type is a reference type, pass this by value, and when it is a value type pass this by ref (or perhaps ref readonly when the member is readonly). In the rare case (<2%) that the underlying type is an unconstrained type parameter, decide at runtime.
  2. Follow classic extension methods: Always pass this by value.

Either way, the default will be wrong for some significant number of extension members on value types! Passing by value prevents mutation. Passing by reference is unnecessarily expensive for small value types.

In order to get to reasonable expressiveness on this, the type-based approach would need to break the abstraction and get a little more "parameter-like" with the underlying type. For instance, the for clause might optionally specify ref or in:

public extension TupleExtensions for ref (int x, int y)
{
    public void Swap() => (x, y) = (y, x); // `this` is by ref and can be mutated
    public readonly int Sum => x + y; // `this` is ref readonly and cannot me mutated
}

Attributes

This-parameters can have attributes. It is quite rare (< 1%), and the vast majority are nullability-related. Of course, extension members can have attributes, but they would need a way to specify that an attribute goes on the implicit this parameter!

One way is to introduce an additional attribute target, say this, which can be put on instance extension members:

[this:NotNullWhen(false)] public bool IsNullOrEmpty => this is null or [];

Nullable reference types

A classic extension method can specify the underlying type as a nullable reference type. It is fairly rare (< 2%) but allows for useful scenarios, since, unlike instance members, extension members can actually have useful behavior when invoked on null. Anotating the receiver as nullable allows the extension method to be called without warning on a value that may be null, in exchange for its body dealing with the possibility that the parameter may be null.

A type-based approach could certainly allow the for clause to specify a nullable reference type as the underlying type. However, not all extension members on that type might want it to be nullable, and forcing them to be split across two extension declarations seems to break with the ideal that nullability shouldn't have semantic impact:

public extension NullableStringExtension for string?
{
    [this:NotNullWhen(false)] public bool IsNullOrEmpty => this is null or [];
}
public extension StringExtension for string
{
    public string Reverse() => ...;
}

It would be better if nullability could be specified at the member level. But how? Adding new syntax to members seems to be exactly what the type-based approach is trying to avoid! The best bet may be using an attribute with the this target as introduced above:

public extension StringExtension for string
{
    [this:AllowNull][this:NotNullWhen(false)] public bool IsNullOrEmpty => this is null or [];
    public string Reverse() => ...;
}

This would allow extension members on nullable and nonnullable versions of the same underlying reference type to be grouped together.

Grouping

Classic extension methods can be grouped together in static classes without regard to their underlying types. This is not the case with the type-based approach, which requires an extension declaration for each underlying type. Unfortunately it is also only partially the case for the member-based approach, as we saw above. Adding e.g. extension properties to MemoryExtensions, which has a lot of open generic underlying types, would lead to it having to be broken up into several extensions declarations.

This is an important quality of classic extension methods that unfortunately neither approach is able to fully bring forward.

Non-extension members

Current static classes can of course have non-extension static members, and it is somewhat common for those to co-exist with extension methods.

In the member-based approach a similar thing should be easy to allow. Since the extension members have special syntactic elements, ordinary static members wouldn't conflict.

In the type-based approach, ordinary member syntax introduces extension members! So if we want non-extension members that would have to be accommodated specially somehow.

Interface implementation

The type-based syntax lends itself to a future where extensions implement interfaces on behalf of underlying types.

For the member-based syntax that would require more design.

Summary

All in all, both approaches have some challenges. The member-based approach struggles with open generic underlying types, which are fairly common. They can be addressed in two different ways, both of which add significant syntax and complexity.

The type-based approach abstracts away parameter details that are occasionally useful or even critical, and would need to be augmented with ways to "open the lid" to bring that expressiveness forward from classic extension methods when needed.

@MadsTorgersen MadsTorgersen self-assigned this Oct 30, 2024
@HaloFour
Copy link
Contributor

HaloFour commented Oct 30, 2024

Just to reiterate the Scala 3 approach, which I think does solve for a lot of these problems (although not necessarily all of them). The extensions are member-based, so it's completely unopinionated regarding the container class into which they are declared. However, it also allows grouping the members by target so you don't have to constantly repeat the target for each member. The extension itself can be declared as generic, allowing for generic targets and constraints, and that generic type parameter is flattened into the member signatures as the first generic type parameter. The extension uses primary constructor syntax, so the target declaration is very flexible as to the parameter name and type, and supports annotations like any normal parameter would.

Scala:

// can be declared in any class
object Enumerable {

  // single member, non-generic target, generic method
  extension (source: Seq[_]) def ofType[T](): Seq[T] = ???
  
  // multiple members, generic target
  extension[T] (source: Seq[T]) {
    // non generic extension method
    def filter(predicate: T => Boolean): Seq[T] = ???

    // generic extension method
    def map[TR](mapper: T => TR): Seq[TR] = ???

    // extension property
    def isEmpty: Boolean = ???

    // extension operator
    def :: (other: Seq[T]): Seq[T] = ???
  }

  // more complicated generics
  extension[T, U] (source: Seq[T]) {
    // generic extension operator
    def + (other: Seq[U]): Seq[T] = ???
  }

  // can also include normal members
  def Empty[T](): Seq[T] = ???
}

I think the one use case it doesn't cover is when you want a generic target but the generic type parameter is not the first generic type parameter. Actually, nevermind, it does:

// reordered generic type parameters
extension[TR, T] (source: Seq[T]) def map(mapper: T => TR): Seq[TR] = ???

I'm also not sure how that could translate to C# syntax. I guess this is somewhat close to the for scoping that was suggested earlier.

Anyway, food for thought. I think by borrowing from and combining existing language syntax elements like this it does help to simplify some of the more complicated cases without having to come up with a bunch of new syntax.

@jubruckne
Copy link

Just some personal thoughts... Seeing how complex these considerations get… why do we even need a new kind of syntax for non-role extensions? I’ve wanted extensions properties once or twice, but really it’s not something that would allow entirely new things, or would it?
It feels to me maybe not worth it to introduce a new keyword and a new category of type for that.

What I’m really looking forward to is „extension types“ / „roles“ with their own type identities. I would use them soooo much.

@0x0737
Copy link

0x0737 commented Oct 30, 2024

Ok, here's something I came up with.
As always, there may be dumb fundamental problems or typos.
Sorry if I missed something.

The idea is to take hybrid approach.
We make all old-style extensions have a corresponding new form which does not require additional syntax to be backwards-compatible. That is, instance extension methods are compatible by default.
We split generic parameters in two separate lists.
The first list is for parameters which are used primary to bind the extension.
The second list is for parameters which are supplied at a call site. Currently, this list is only for methods, because other members can't specify generic paremeters.
The actual implementation of the member can take parameters from both lists.

Meet the new

"for" clause

A "for" clause specifies the generic parameters which participate in binding of a member, the underlying type, the receiver mode and its modifiers.
At a call site, parameters from the "for" clause are already known for instance members.
Parameters from the method declaration are specified by the caller or inferred from the arguments.
I probably need to clarify: The motivation for splitting parameters is not only to make uniform syntax possible, but to remove the need to specify already known binding type parameters when you call extension methods with additional type parameters through instance syntax.

Receiver modes

Receiver mode is an extension-only concept which determines how you will access the underlying type or the object for which your extension is called.

I will describe 4 possible receiver modes, 2 for instance and 2 for static members.
Actually it is sufficient to implement two, the 1st and either the 3rd or the 4th.

For instance members:

  1. <parameter name>: The extended object is passed as a parameter with the name you provide. The body of the extension sees it as a normal parameter with that name. This is similar to old extension methods.
  2. this: The extended object is passed as a parameter with the name "this". The body of the extension sees it as a receiver. The extension can directly access any of its public members like if they were declared right in the extension type or use "this.Member" notation.

For static members:

  1. default: The extended type's members are not directly accessible in the extension body. You can use already declared generics to repeat whatever is in your underlying type declaration to mention the type you are extending. This receiver mode kinda represents an absence of a receiver.
  2. self: This can be used as a good starting point to implement the self-type feature. The extended type's members can be directly accessed in the extension body. The keyword 'self' in the body is a shorthand for whatever is in your underlying type declaration. No actual new possibilities in this context, just sugar.
public extension ExtensionA
{
    // T goes into "for" because it is used in the underlying type
    public void ExtMethod0(int someArg) for <T> List<T> list where T : class?
    {
        // "list" is a parameter now.
        // You can access its members only by referring to them like 'list.Member'
        // T is a type you get from binding when you call it like an instance method
    }

    public void ExtMethod1(int someArg) for <T> List<T> this where T : class?
    {
        // "this" is the receiver now. 
        // You can access its members by referring to them like 'this.Member' OR
        // just 'Member' like if they were declared right here in the extension
        // T is a type you get from binding when you call it like an instance method
    }

    // Generic parameters not used in the receiver type are specified in the method declaration.
    // If generic parameters are present in both the "for" clause and the method declaration, their constraints are specified in a shared "where" clause.
    public void ExtMethod2<U>(T someArg1, U someArg) for <T> List<T> this where T : class? where U : struct
    {
        // "this" is the receiver now. 
        // You can access its members by referring to them like 'this.Member' OR
        // just 'Member' like if they were declared right here in the extension
        // T is a type you get from binding when you call it like an instance method
        // U is whatever specified at the call site
    }

    // This is how you include receiver modifiers in a "for" clause.
    // It doesn't matter whether it's a named parameter or a this-receiver
    public void ExtMethod3(T val) for <T> [SomeAttribute] ref T this where T : struct
    {
        // "this" is the receiver now. It is passed by-ref and has [SomeAttribute]
        // You can access its members by referring to them like 'this.Member' OR
        // just 'Member' like if they were declared right here in the extension
        // T is a type you get from binding when you call it like an instance method
        this = val;
    }

    // This is how you declare instance extension properties
    // You always have to use 'this' as the receiver mode, since named parameter
    // doesn't make sense here.
    public int ExtProperty for <T> T this where T : class? => 1;

    // And indexers (why they still can't named?)
    // Although we have parameters here, let's not complicate further and only use 'this'. 
    public object this[int i] for <T> T this where T : class?
    {
        get ... set ...
    }

    // And static methods
    // You don't have to specify 'default' receiver mode in this context.
    public static T StaticExtMethod() for <T> T where T : class?
    {
        return ...
    }

    // self may be useful when you have a long underlying type which you don't want to repeat to access its members
    public static T StaticExtMethod2() for <T> TypeA<TypeB<TypeC<T>>> self where T : class?
    {
        UnderlyingTypeStaticMethod1()
        UnderlyingTypeStaticMethod2()
        self.UnderlyingTypeStaticMethod3()
        return ...
    }

    // And operators
    public static explicit operator int(T obj) for <T> T where T : class?
    {
        return obj.GetHashCode();
    }

    // You got the idea
}

How to call these?

List<object> list = new();
int arg = 0;

// 0
list.ExtMethod0(arg);

// 1
list.ExtMethod1(arg);

// 2
list.ExtMethod2<object, int>(new object(), arg);
// Important: See Update 3 for more info.
list.ExtMethod2<.., int>(new object(), arg);

// 3
arg.ExtMethod3(1);

// Instance Property
_ = list.ExtProperty;

// Instance Indexer
_ = list[0];

// Static methods
_ = object.StaticExtMethod();
_ = TypeA<TypeB<TypeC<object>>>.StaticExtMethod2();

// Operator
_ = (int)(new object());

How to call these like static members on the extension type?

When you call new extension methods like static ones,
they appear to you like old ones. The "for" clause parameters are joined with the method parameters.

// 0
ExtensionA.ExtMethod1(list, arg);

// 1
ExtensionA.ExtMethod1(list, arg);

// 2
// Since this method requires additional parameter which cannot be inferred, you have to fill in generics explicitly.
ExtensionA.ExtMethod2<int, int>(list, new object(), arg);

// 3
ExtensionA.ExtMethod3(ref arg, 1);

// The other member kinds don't require compatibility with old extensions.
// They still can be lowered to static members of the extension type.

Now, let's convert that extension to multiple type-based ones, which will be identical to previous

The "for" clause has moved to the extension type declaration
The extension itself is always a static type.
Why parameters for the underlying type are still specified in the "for" clause?
Because we need to be able to do old-style calls of static methods on the extension type and still have the benefit of not repeating the underlying type in the extension members:

ExtensionA.ExtMethod1(receiver, arg) vs ExtensionA<int>.ExtMethod1(receiver, arg)

But we don't have generic extension types in C#? Well, in type-based approach we will, so this is required to have compatibility and to preserve extension syntax uniformity.

public extension ExtensionA for <T> List<T> where T : class?
{
    public void ExtMethod0(int someArg) for list
    {
        // "list" is a parameter now.
        // You can access its members only by referring to them like 'list.Member'
        // T is a type you get from binding when you call it like an instance method
    }

    public void ExtMethod1(int someArg) for this
    {
        // "this" is the receiver now. 
        // You can access its members by referring to them like 'this.Member' OR
        // just 'Member' like if they were declared right here in the extension
        // T is a type you get from binding when you call it like an instance method
    }

    public void ExtMethod2<U>(T someArg1, U someArg) for this
    {
        // "this" is the receiver now. 
        // You can access its members by referring to them like 'this.Member' OR
        // just 'Member' like if they were declared right here in the extension
        // T is a type you get from binding when you call it like an instance method
        // U is whatever specified at the call site
    }
}

public extension ExtensionB for <T> T where T : struct
{
    public void ExtMethod3(T val) for [SomeAttribute] ref this
    {
        // "this" is the receiver now. It is passed by-ref and has [SomeAttribute]
        // You can access its members by referring to them like 'this.Member' OR
        // just 'Member' like if they were declared right here in the extension
        // T is a type you get from binding when you call it like an instance method
        this = val;
    }
}

public extension ExtensionC for <T> T where T : class?
{
    public int ExtProperty for this => 1;

    public object this[int i] for this
    {
        get ... set ...
    }

    // 'default' is one of the static receiver modes and also literally means empty 'for' clause. This is in line with the 'default' generic constraint with similar meaning. It is just there to mark the method as an extension.
    // Options for different naming may include '_', 'static', but personally I find 'default' the best.
    // Also, since we already have a parent 'for' scope, there is an option to omit the 'for' clause here and make all static members in a 'for' scope extensions by default. I don't like this approach because it makes it impossible to declare non-extension statics if you use a type-based 'for' scope without extracting it to be a child scope like will be shown later.
    // And finally an augmentation of the previous way: Another method modifier could be added which means "nonextension".
    public static T StaticExtMethod() for default
    {
        return ...
    }

    public static explicit operator int(T obj) for default
    {
        return obj.GetHashCode();
    }
}

public extension ExtensionD for <T> TypeA<TypeB<TypeC<T>>> where T : class?
{
    public static T StaticExtMethod2() for self
    {
        UnderlyingTypeStaticMethod1()
        UnderlyingTypeStaticMethod2()
        self.UnderlyingTypeStaticMethod3()
        return ...
    }
}

Multiple 'for' scopes in an extension

Multiple for scopes can be specified in an extension type to include extensions for different types. A for clause on an extension type, as shown in the former example, also denotes a for scope, it's simply merged with the type declaration. for scopes can't be nested and they can't include parameter modifiers or receiver mode. These properties belong to for clauses of each individual member.

public extension ExtensionA
{
    for <T> List<T> where T : class?
    {
        public void ExtMethod0(int someArg) for list
        {
            // "list" is a parameter now.
            // You can access its members only by referring to them like 'list.Member'
            // T is a type you get from binding when you call it like an instance method
        }

        public void ExtMethod1(int someArg) for this
        {
            // "this" is the receiver now. 
            // You can access its members by referring to them like 'this.Member' OR
            // just 'Member' like if they were declared right here in the extension
            // T is a type you get from binding when you call it like an instance method
        }

        public void ExtMethod2<U>(T someArg1, U someArg) for this
        {
            // "this" is the receiver now. 
            // You can access its members by referring to them like 'this.Member' OR
            // just 'Member' like if they were declared right here in the extension
            // T is a type you get from binding when you call it like an instance method
            // U is whatever specified at the call site
        }
    }
    
    for <T> T where T : struct
    {
        public void ExtMethod3(T val) for [SomeAttribute] ref this
        {
            // "this" is the receiver now. It is passed by-ref and has [SomeAttribute]
            // You can access its members by referring to them like 'list.Member' OR
            // just 'Member' like if they were declared right here in the extension
            // T is a type you get from binding when you call it like an instance method
            this = val;
        }
    }

    for <T> T where T : class?
    {
        public int ExtProperty for this => 1;

        public object this[int i] for this
        {
            get ... set ...
        }

        public static T StaticExtMethod() for default
        {
            return ...
        }

        public static explicit operator int(T obj) for default
        {
            return obj.GetHashCode();
        }
    }

    for <T> TypeA<TypeB<TypeC<T>>> where T : class?
    {
        public static T StaticExtMethod2() for self
        {
            UnderlyingTypeStaticMethod1()
            UnderlyingTypeStaticMethod2()
            self.UnderlyingTypeStaticMethod3()
            return ...
        }
    }
}

Update 1: We can make grouping work here too. Moved.

Update 2: I no longer call "this" a parameter and a receiver at the same time. "this" is a fully-fledged receiver, you should be able to call its methods like this: Moved.

Update 3: On generic parameters in instance syntax

There is a problem when it comes to the instance syntax for methods.
With old-style extensions the only way you can specify type arguments in
instance calls is to specify all of them or none. But in the context of this
"proposal" you can only specify those which are directly declared on the method.
And here that problem arises.
We have to support the old syntax where you specify all arguments too.
But if we were to support both ways, adding a new method with the same signature besides a different number of generic parameters would become a breaking change.

Suppose we have a call obj.A<object>()

And extension:

public void A() for <T> T

If we add a new extension

public void A<U>() for <T> T

obj.A<object>() should mean the same A with no method-decl params. So, parameters from the for clause could have priority over method-decl ones.

But if we had that extension initially

public void A<U>() for <T> T

obj.A<object>() would mean the A with one method-decl param.

So adding

public void A() for <T> T

becomes a breaking change, as obj.A<object>() now calls the A with no method-decl params.

This is a nasty compatibility problem which I overlooked and unfortunately I can't see a really clean way to counter it.

One way we can make it is to remove the ability to do instance calls without filling for-clause generic params and require something explicit like obj.A<.., object>() if we want to do an instance extension call with generics filled partially. ".." would mean "automatically fill in all for-clause generic parameters"
This way we kind of preserve the benefit of split generics.

Update 4: Examples, support both named parameter and this-receiver

Initially I supposed that only this-receiver form will be used with new extensions.
However to

  1. Avoid requiring attributes to specify this-parameter name in metadata for compatibility
  2. Avoid rewriting code to refer to "this" instead of the parameter
  3. Make syntax more explicit

I decided to change this part of design and introduce a concept of receiver modes.
The main part of the post is updated to incorporate this change.

Examples

1.

public partial extension Enumerable
{
    for <TSource> IEnumerable<TSource>
    {
        public IEnumerable<IGrouping<TKey, TElement>> GroupBy<TKey, TElement>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector) for source => ...;
    }
}

lowers to

public static partial class Enumerable
{
    public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector) => ...;
}

2.

public partial extension MemoryExtensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Span<T> AsSpan(int start, int length) for <T> T[]? array
    {
        return new Span<T>(array, start, length);
    }
}

lowers to

public static partial class MemoryExtensions
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Span<T> AsSpan<T>(T[]? array, int start, int length)
    {
        return new Span<T>(array, start, length);
    }
}

Update 5

If it is not clear from the post, one of the goals of this design is to support both the type and member approaches simultaneously allowing you to choose which one is a better fit for your use case. They are not presented as separate design options here.

@hez2010
Copy link

hez2010 commented Oct 30, 2024

We can introduce a self type for the type being extended and borrow the method type parameter syntax for self type. Here the self type will be substituted by the actual type being extended.

extension_member: ... identifier self_type? '(' parameter_list ')'
generic_type_param_with_self: '<' self_type? generic_type_param '>'
self_type: attribute_list? ('ref' | 'ref readonly' | 'in')? 'self' '?'? '*'?
generic_type_param: ',' attribute_list? identifier generic_type_param?
attribute_list: '[' attribute ']' attribute_list?
attribute: attr_type... ','?
public extension TupleExtensions for (int x, int y)
{
    public void Swap<ref self>() => (x, y) = (y, x); // `this` is by ref and can be mutated
    public int Sum<ref readonly self>() => x + y; // `this` is ref readonly and cannot me mutated
    public int Sum<self*>() => this->x + this->y; // `this` is a pointer
}

public extension StringExtension for string
{
    public string Reverse() => ...; // instance member
    public string Reverse2<self>() => ...; // instance member, but with an explicit self, which is equivalent to `Reverse`
    public static string Create() => ...; // static member
    public bool IsNullOrEmpty<[NotNullWhen(false)] self?>() => this is null or []; // this is string?
}

public extension GenericExtension<T> for Foo<T> where T : struct
{
    public U Bar<[Foo] ref self, U>() where U : class => ...; // some generic example
}

Just like extension methods we already have today where we require users to put a this parameter as the first parameter, in the new extensions, we can require users to put a self type parameter as the first type parameter (it's not a real type parameter though).

I think this should be able to handle all the cases we want to cover.


Alternatively, we can still introduce a self type and require users to put it on the first parameter if the extension member is an instance member:

public extension TupleExtensions for ref (int x, int y)
{
    public void Swap() => (x, y) = (y, x); // `this` is by ref and can be mutated
    public readonly int Sum => x + y; // `this` is ref readonly and cannot me mutated
}
public extension NullableStringExtension for string?
{
    [this:NotNullWhen(false)] public bool IsNullOrEmpty => this is null or [];
}
public extension StringExtension for string
{
    public string Reverse() => ...;
}

becomes

public extension TupleExtensions for (int x, int y)
{
    public void Swap(ref self) => (x, y) = (y, x); // `this` is by ref and can be mutated
    public int Sum(ref readonly self) => x + y; // `this` is ref readonly and cannot me mutated
}
public extension StringExtension for string
{
    public bool IsNullOrEmpty([NotNullWhen(false)] self?) => this is null or [];
    public string Reverse(self) => ...;
}

and an extension member without self as the first parameter becomes static automatically.

public extension StringExtension for string
{
    public string Reverse(self) => ...; // instance member
    public string Create() => ...; // static member
}

However, in this alternative, we don't use static to specify whether a member is static or not. But extensions are new concept in C#, I do think it can apply some different rules than existing types.

@alrz
Copy link
Member

alrz commented Oct 30, 2024

There's no dedicated thread on disambiguation mechanism but I wanted to bring it up as I just hit this with ToListAsync extension in efcore and mongo driver.

I believe the cast operator was considered for properties ((E)e).Prop which supposedly works for methods as well but it has the same problem with await: it's a prefix operator.

I think a postfix operator is ideal as the point of an extension is to be able to dot off an expression and chain members.

e.(MongoQueryable).ToListAsync()

Using golang syntax there to demonstrate but anything similar is much easier to read and write, perhaps opens up the discussion on postfix await as well.

@HaloFour
Copy link
Contributor

@alrz

I believe it had been settled that extensions aren't types that you can declare or cast a value to, so that would mean disambiguation would happen like they do with extension methods today, as a static method invocation, e.g. E.ToListAsync(e)

@alrz
Copy link
Member

alrz commented Oct 30, 2024

I believe it had been settled that extensions aren't types that you can declare or cast a value to

This wouldn't be a proper "cast" in that sense though, I'm referring to this proposal:
https://github.com/dotnet/csharplang/blob/main/proposals/extensions_v2.md#disambiguation

If that option is no longer on the table, how extension properties are going to be disambiguated? E.get_Prop(e)?

@HaloFour
Copy link
Contributor

HaloFour commented Oct 30, 2024

@alrz

If that option is no longer on the table, how extension properties are going to be disambiguated? E.get_Prop(e)?

I think so, although I guess everything is on the table at the moment. There have been so many meetings/discussions on extensions recently that it all kinda blends, but it sounds like extensions won't be "types" in their own right, and if you can't have a value of the extension type does it make sense to be able to have a pseudocast to an extension type?

@alrz
Copy link
Member

alrz commented Oct 30, 2024

If extensions lower to a static class, the no-cast role is already in place so that wouldn't be something new and is consistent with the actual emitted code.

if you can't have a value of the extension type does it make sense to be able to have a pseudocast to an extension type?

The proposal does mention this confusion as a drawback so I assume the syntax is not final. I am NOT proposing to add a new cast-operator, but I do think a postfix form works best when talking about extensions.

@HaloFour
Copy link
Contributor

The proposal does mention this confusion as a drawback so I assume the syntax is not final. I am NOT proposing to add a new cast-operator, but I do think a postfix form works best when talking about extensions.

We shall see what happens. I assume that extension disambiguation will probably remain a relatively rare thing, so I doubt it would move the needle a whole lot on the existing proposals for postfix cast or await anymore than any of the existing use cases.

@TahirAhmadov
Copy link

TahirAhmadov commented Oct 30, 2024

A classic extension method can specify the underlying type as a nullable reference type. It is fairly rare (< 2%) but allows for useful scenarios, since, unlike instance members, extension members can actually have useful behavior when invoked on null.

Something being useful, especially when it's not widely used already, shouldn't be enough of a justification to take it into account when designing a new feature - extensions. Given that it's merely a syntactic difference which can account for at most a few extra keystrokes (like the previously discussed ((string?)null).LetterCount vs ((string?)null)?.LetterCount ?? 0, or s.IsNullOrEmpty() vs string.IsNullOrEmpty(s), the size of the monkey wrench being thrown into the mix just doesn't outweigh the benefits, IMO.

Regarding being "useful", there have been requests (at least once or twice that I've seen) to allow things like:

string? s = null;
// ...
if (s) // instead of if(s != null)
{
  // ...
}

The usefulness of the above "implicit not-null-truthiness" is similar to the usefulness of accessing members on a null value, yet C# (rightfully!) rejects this idea. Similarly, I think C# should reject the idea of instance members on null values.

Regarding ref:

public extension TupleExtensions for ref (int x, int y)
{
    public void Swap() => (x, y) = (y, x); // `this` is by ref and can be mutated
    public readonly int Sum => x + y; // `this` is ref readonly and cannot me mutated
}

I like this a lot, but I do want to point out a minor complication, imagine the below:

public extension TupleExtensions for ref (int x, int y)
{
    public void SwapAndAdd() => (x, y) = (y + this.Sum, x + this.Sum); // `this` is by ref and can be mutated
}
public extension TupleExtensions for (int x, int y)
{
    public int Sum => x + y;
}

In the SwapAndAdd, it would have to create a local copy in order to call the Sum (which is an extension on the value type in this scenario), which introduces a performance hit. The solution can be to stipulate that the Sum is emitted twice, once in native form (by value) and once more in by ref form, and then SwapAndAdd can then call the by ref form behind the scenes, avoiding a copy.

We couldn't find a reasonable way to represent interface-implementing extensions in the runtime.

If this is caused by the requirement that the received interface reference can be reference equal (and "identity equal" in other ways, like GetType()) to the underlying instance, then I would re-iterate my previously stated opinion on the matter: giving up on such a useful feature for the arguably very few edge cases where reference equality is expected to be used in this way, is not justified. I'm pretty sure most people would prefer to have extension interfaces without identity equality, than no extension interfaces at all. And the former can be trivially achieved with what's available in the runtime today.

TLDR, I think we should go with the syntactically type based approach, and choose the correct emit strategy which satisfies the requirements in the best possible way technically, leaving it to be an implementation detail. Backwards compatibility can be easily achieved by leaving existing extension methods as redirect stubs, and thought-out management of overload resolution.

@CyrusNajmabadi
Copy link
Member

yet C# (rightfully!) rejects this idea

Humorously, extensions would enable this. As you'd now be any to add extension conversions to bool, as well as operator true/false.

@TahirAhmadov
Copy link

PS. Having said "implementation detail", I realized that for other languages to be able to use these new C#-compiled extensions, we may want to spec the emitting strategy after all.

@TahirAhmadov
Copy link

yet C# (rightfully!) rejects this idea

Humorously, extensions would enable this. As you'd now be any to add extension conversions to bool, as well as operator true/false.

I thought about that a few days ago myself 🤣 This is less than ideal, but ultimately, it would be a particular team engaging in such shenanigans, and C# cannot possibly stop all the foot-guns out there.

@CyrusNajmabadi
Copy link
Member

It's not a foot gun. These conversations exist exactly to allow these scenarios.

I know from previous conversations with you that there are things that are allowed that you don't like. But these aren't mistakes, or things that accidentally crept in. They were explicit design decisions to enable these types of scenarios.

We had the choice to block them, and we thought about it and explicitly said that we wanted people to be able to do this. Since then people have done this, and everything has been completely fine. The only argument I've seen so far is that someone doesn't like it.

But that's an argument for removing practically everything in the language.

@TahirAhmadov
Copy link

I guess we have a philosophical difference on this subject. I think one of the original objectives of C# to always try creating pits of success and avoid creating pits of failure, is extremely valuable.
If a particular limitation prevents a specific use case, I'm all for looking into it - everybody can speak up here and request something or complain about limitations (and people do). And in some cases, lifting existing limitations makes sense.
My argument for not allowing accessing instance extension members on nulls is not merely a personal preference; not a matter of "taste". I think the inconsistency with native instance members in this regard is an objective argument against such syntax. I know, "consistency" is not the end-all-be-all, but in this case, we are explicitly saying, "these members are designed to supplement existing members". Yet they follow a completely different methodology WRT nullability?
Besides, it's easier to ship the feature without calling instance members on null, and look at community feedback later. If there are hoards of people asking for this, you guys can say - thanks for your input, but we have many more voices who want this, so we'll add it, and at the end of the day, it doesn't really "harm" me personally that much. In fact, it's a win-win: we ship the feature quicker now; we can potentially end up with a design which is more "pit of success"; and if people really want it, it can be debated together with other requests, per the usual design conveyor.

@HaloFour
Copy link
Contributor

@TahirAhmadov

Besides, it's easier to ship the feature without calling instance members on null

It would require more design work, and more runtime overhead, for the compiler to try to prevent this.

People have already decided with extension methods that they want this. I want this, and I do use it. We're not relitigating the concept of extensions, we're extending it.

@CyrusNajmabadi
Copy link
Member

I think one of the original objectives of C# to always try creating pits of success and avoid creating pits of failure, is extremely valuable.

I think that is a major design goal. The difference here is that I look at the outcomes of our intentional design decisions and see that they were successful. So why start blocking things that were not a mistake?

Note: the same concerns you have here were the ones levied by many in the community about extension methods entirely. The idea that you appear to have instance methods exposed that you did not write was so completely and obviously wrong to some people, it was literally predicted that the feature would cement the death of the language. :-)

I look at the flexibility we offered, and I see how the community has used it well. And I don't see a good reason to take away.

@CyrusNajmabadi
Copy link
Member

I think the inconsistency with native instance members in this regard is an objective argument against such syntax.

This was strongly considered and discussed at the time. It was not accidental. And there were a few people that thought the same, including wanting null checks automatically emitted.

In the end, the benefits of letting methods execute on null far outweighed those concerns. Since then, we've had 20 years to reflect on this and to see how the community would do things. Would they reject such methods. Would they make analyzers that require a null+throw check.

In the end, that didn't happen. The community embraced this capability, and they showed that flexibility in extensions could be used to great effect.

You may find that a pity. But that's really not selling me on breaking things that have been part and parcel of this design space for decades now :-)

@CyrusNajmabadi
Copy link
Member

@TahirAhmadov

Besides, it's easier to ship the feature without calling instance members on null

It would require more design work, and more runtime overhead, for the compiler to try to prevent this.

People have already decided with extension methods that they want this. I want this, and I do use it. We're not relitigating the concept of extensions, we're extending it.

thanks for your input, but we have many more voices who want this, so we'll add it, and at the end of the day

Note, doing this in a non-breaking way would be challenging. People would absolutely start depending on that, and then we would not be able to change it. So we'd need new syntax for that capability.

@TahirAhmadov
Copy link

I think the inconsistency with native instance members in this regard is an objective argument against such syntax.

This was strongly considered and discussed at the time. It was not accidental. And there were a few people that thought the same, including wanting null checks automatically emitted.

In the end, the benefits of letting methods execute on null far outweighed those concerns. Since then, we've had 20 years to reflect on this and to see how the community would do things. Would they reject such methods. Would they make analyzers that require a null+throw check.

In the end, that didn't happen. The community embraced this capability, and they showed that flexibility in extensions could be used to great effect.

You may find that a pity. But that's really not selling me on breaking things that have been part and parcel of this design space for decades now :-)

The exact same thing can be written 20 years after string? s = null; if(s) { ... } is allowed natively in the language: lone voices against it, overwhelming embrace by the community, and expansion of the language in a similar direction (who says we cannot implicitly convert a string to an int, if the string represents a valid int? it's useful, people love it, etc.! ) 🤣

People have already decided with extension methods that they want this. I want this, and I do use it. We're not relitigating the concept of extensions, we're extending it.

Extensions are a new feature. It's not merely an "extension" (haha) of extension methods.

Anyway, I wrote everything there is to write about this.

@HaloFour
Copy link
Contributor

@TahirAhmadov

Extensions are a new feature. It's not merely an "extension" (haha) of extension methods.

I disagree, but even if that were the case I don't see why the team would make a different decision here. I understand that you disagree with the decision, but they (and I) consider it to be a success, thus it would make sense to retain that same capability here.

@DanFTRX
Copy link

DanFTRX commented Oct 30, 2024

Is there going to be a mechanism by which to indicate to nullability analysis that an extension method does not accept null? Or are all extensions providing functionality that doesn't make sense without an instance going to require null guard boilerplate to play well with nullability analysis?

@HaloFour
Copy link
Contributor

@DanFTRX

Is there going to be a mechanism by which to indicate to nullability analysis that an extension method does not accept null? Or are all extensions providing functionality that doesn't make sense without an instance going to require null guard boilerplate to play well with nullability analysis?

That already exists with extension methods today. If the extension target is a non-nullable reference type, you'll get a warning if you try to invoke that extension.

@0x0737
Copy link

0x0737 commented Oct 30, 2024

I would be grateful if someone experienced checked my "proposal" here for potential "holes".
Just to know if there are already no-go's and I should drop it.
Or maybe it is not clear enough and looks insane.

@TheUnlocked
Copy link

So if I'm reading this right, neither approach will actually let us implement interfaces with extensions? To me that was the primary benefit of dedicated extensions, and without that, I'm not sure this whole thing is worth it.

If people are really clamoring for extension properties/operators then a member-only approach seems preferable, ideally as close to the existing extension method syntax as possible.

@CyrusNajmabadi
Copy link
Member

If people are really clamoring for extension properties/operators

This is the request. And it's not a random smattering either. It's teams like the runtime, which very much wants to design apis that take advantage of that :-)

@MauNguyenVan
Copy link

Dart lang has a good design for extension https://dart.dev/language/extension-methods
and extension type https://dart.dev/language/extension-types.
IMO, Extension type look good and elegant

@HaloFour
Copy link
Contributor

@0x0737

I would be grateful if someone experienced checked my "proposal" here for potential "holes".

The exercise I've been going through is how existing extension methods would map to any new syntax, and how that lowers to something that doesn't result in either binary or source breaking changes. Particularly cases like System.Linq.Enumerable and System.MemoryExtensions where some of the challenges have been identified.

@BarionLP
Copy link

Also with these ideas, will it still be possible to define aliases?

extension IntDictionary<TValue>(Dictionary<int, TValue>);

@HaloFour
Copy link
Contributor

Also with these ideas, will it still be possible to define aliases?

No, this feature is unrelated to aliases.

@aradalvand
Copy link
Contributor

aradalvand commented Nov 16, 2024

PLEASE, instead of the latest 3-level deeply nested abomination of an eye-bleeder:

public static partial class MyExtensions
{
    extension(string source)
    {
        public void Whatever() { ... }
    }

    extension(int source)
    {
        public void Whatever() { ... }
    }
}

Why not just do this?! Allow one extension name to be shared across multiple extension declarations:

public extension MyExtensions(string source)
{
     public void Whatever() { ... }
}

public extension MyExtensions(int source)
{
    public void Whatever() { ... }
}

Best of all worlds.

  • This decreases one ugly level of nesting.
  • All extensions with the same name are lowered to the same static class, offering binary compatibility with legacy extension methods.
  • Paves the way for extensions implementing interfaces
  • Doesn't have the "type explosion" problem that the old type-based extensions have since all the extensions with the same are lowered to the same static class.
  • Has all the benefits of type-based extensions like being isomorphic with regular members.

@KennethHoff
Copy link

KennethHoff commented Nov 16, 2024

I would, at very least, require the use of partial in this example.

One of the reasons for the nested extension scope is to allow sharing of common code between extensions by putting it in the class directly; would you then be able to create another static class manually with the same name?

public static partial class MyExtensions
{
    private string MyStringUtility() { ... }

    private const int MyRandomConstant = 5;
}

public partial extension MyExtensions(string source)
{
     public void Whatever() { ... }
}

public partial extension MyExtensions(int source)
{
    public void Whatever() { ... }
}

At which point, which is more intuitive?

public static class MyExtensions
{
    private string MyStringUtility() { ... }

    private const int MyRandomConstant = 5;

    extension (int source) 
    {
        public void Whatever() { ... }
    }

    extension (string source) 
    {
        public void Whatever() { ... }
    }
}

Personally, I find the latter more palettable, despite having another level of nesting.

Ideally, they'd implement file-scoped classes at some point, in which case we'd get something like this:

public static class MyExtensions;

private string MyStringUtility() { ... }

private const int MyRandomConstant = 5;

extension (int source) 
{
    public void Whatever() { ... }
}

extension (string source) 
{
    public void Whatever() { ... }
}

@hez2010
Copy link

hez2010 commented Nov 16, 2024

Yeah, I also completely don't get the point why we should nest the extension under a static class, which is extremely ugly.
We already have partial keyword, why not reuse the partial so that an extension can be defined at the top-level like this:

public partial extension MyStringExtension(string str)
{
    public void Foo() { ... }
}

We can allow the nullability and attributes on the extended type to be different across all the partial extensions so that we don't need to define a new name for all those variants on a type:

public partial extension MyStringExtension(string str)
{
    public void Foo() { ... }
}
public partial extension MyStringExtension(string? str)
{
    public void Bar() { ... }
}
public partial extension MyStringExtension([Foo] string str)
{
    public void Baz() { ... }
}

This resolves all the issues we have now:

  1. We get a parameter name naturally
  2. We have the extension name set explicitly
  3. We can reuse the extension name for the same type with different attributes and nullability
  4. We don't need to nest the extension under a static class

And furthermore, if we want to, we can even extend it to allow different types to share a same extension name:

public partial extension MyExtension(string str)
{
    public void Foo() { ... }
}
public partial extension MyExtension(int number)
{
    public void Bar() { ... }
}
public partial extension MyExtension(IEnumerable<int> ints)
{
    public void Baz() { ... }
}

As for allowing defining traditional extension methods in an extension type: why is it necessary? Once we have the new extension types, we can migrate to the new one from the old extension methods immediately, allowing such thing comes from no sense.

@aradalvand
Copy link
Contributor

aradalvand commented Nov 16, 2024

I would, at very least, require the use of partial in this example.

No, I wouldn't. The "declarational extendability" of extensions would be an intrinsic characteristic.

One of the reasons for the nested extension scope is to allow sharing of common code between extensions by putting it in the class directly

Put your "common" code in another static class and get it over with; the aesthetics of the normal case shouldn't be sacrificed for such minor cases that could have trivial workarounds.

Ideally, they'd implement file-scoped classes at some point

They won't.

@xamir82
Copy link

xamir82 commented Nov 16, 2024

I too am in favor of @aradalvand's proposal, I viscerally hate the nested "extension" style, it would be an eyebrow-raiser to any newcomer, and is not at all intuitively clear why an extension declaration has be put into a static class, as opposed to it being its own thing.

@HaloFour
Copy link
Contributor

HaloFour commented Nov 16, 2024

All of those forms have their own problems, which become pretty obvious once you make at attempt to port existing extension methods over to the new form, such as System.Linq.Enumerable. Generics immediately complicate things.

If extensions were being designed for the first time now I think the language could take a very different and simpler approach here, but that's not the case. We have an huge ecosystem of extensions which needs to be taken into consideration.

@CyrusNajmabadi
Copy link
Member

I viscerally hate the nested "extension" style

People viscerally hated normal extension methods when we introduced them fwiw. There were many saying they would refuse to upgrade to c# 3, or use them at all. That's sometimes life :-)

@aradalvand
Copy link
Contributor

aradalvand commented Nov 16, 2024

All of those forms have their own problems, which become pretty obvious once you make at attempt to port existing extension methods over to the new form, such as System.Linq.Enumerable. Generics immediately complicate things.

@HaloFour In the style I provided do they really? You could port the entirety of System.Linq.Enumerable cleanly without any breaking changes.

For example, System.Linq.Enumerable contains these two methods:

public static class Enumerable
{
    public static bool All<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
    public static double? Sum(this IEnumerable<double?> source);
}

This would become:

public extension Enumerable<TSource>(IEnumerable<TSource> source)
{
    public bool All(Func<TSource, bool> predicate);
}
public extension Enumerable(IEnumerable<double> source)
{
    public double? Sum();
}

The lowered code would be more or less identical to the currently-lowered version of System.Linq.Enumerable.

@CyrusNajmabadi
Copy link
Member

and is not at all intuitively clear why an extension declaration has be put into a static class, as opposed to it being its own thing.

There has been no statement that it cannot be its own thing. But if we do such a syntactic nicety, it needs to be layered on a system that can solve all the use cases we know need to be solved.

I'm other words, we need to solve for functionality and capabilities first. Syntactic pleasantries can be built on top of that. Bikeshedding that now isn't helpful though as we have to make sure the foundation is powerful and flexible enough first for all our needs.

@CyrusNajmabadi
Copy link
Member

the aesthetics of the normal case shouldn't be sacrificed

There has been no sacrifice of anything.

@HaloFour
Copy link
Contributor

@aradalvand

That creates confusion as you are using the syntax to declare a generic type, but you're not declaring a generic type. That was considered problematic, which is why the generic type parameter in some of these alternative proposals appears somewhere else entirely.

@CyrusNajmabadi
Copy link
Member

This would become:

You need mechanisms for different generics, or the same generics with tons like different constraints. There is also broad apprehension around a generic declaration producing a non-generic final type.

But, firmly, you are on step 10, while we're on step 1. We have to have a system that accomplishes all our needs first. At that point syntactic bike shedding can occur.

We fully expect that we might have an extremely powerful system here with knobs for everything. And we then may layer syntactic conveniences on things for common cases. But we're not designing the latter before we've solved the former.

@aradalvand
Copy link
Contributor

aradalvand commented Nov 16, 2024

That creates confusion as you are using the syntax to declare a generic type, but you're not declaring a generic type.

@HaloFour How does that create confusion?!

If you don't like <> coming after the extension name, fine, do this:

public extension<T> Enumerable(IEnumerable<T> source)
{

}

@CyrusNajmabadi
Copy link
Member

How does that create confusion?!

Up till now, all generic declarations made generic symbols. This would not do that.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Nov 16, 2024

coming after the extension name,

Again, syntactic bike shedding comes last. We're focusing on capabilities and requirements. We know all the syntaxes were strawmanning with can have simpler alternatives for simple cases.

fine, do this:

Being able to lift 'extension' and apply it to a single member is something we've had in mind and are amenable to.

But we are not designing that now. You're focusing on the area we generally do last, as it's full of opinions, subjectivity, and wildly playing around with things. Right me we are firmly concerned with capabilities and flexibility. We want to ensure the new extension system subsumes the old one, provides back compat (source and binary), adds the new features we want, and has an the capabilities are partner teams are asking for.

@xamir82
Copy link

xamir82 commented Nov 16, 2024

syntactic bike shedding comes last

Isn't this whole issue centered around discussion about different syntactic styles and their ramifications?

@hez2010
Copy link

hez2010 commented Nov 16, 2024

Takes an example on the System.Linq, if we allow extensions to share the name for different underlying type, all the issues would be resolved:

public partial extension Enumerable<TSource>(IEnumerable<TSource> source)
{
    public TSource Aggregate(Func<TSource, TSource, TSource> func) { }
    public TAccumulate Aggregate<TAccumulate>(TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func) { }
    public TResult Aggregate<TAccumulate, TResult>(TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector) { }
    public bool All(Func<TSource, bool> predicate) { }
    public bool Any() { }
    public bool Any(Func<TSource, bool> predicate) { }
    public IEnumerable<TSource> Append(TSource element) { }
    public IEnumerable<TSource> AsEnumerable() { }
    public decimal Average(Func<TSource, decimal> selector) { }
    public double Average(Func<TSource, double> selector) { }
    public double Average(Func<TSource, int> selector) { }
    public double Average(Func<TSource, long> selector) { }
    public decimal? Average(Func<TSource, decimal?> selector) { }
    public double? Average(Func<TSource, double?> selector) { }
    public double? Average(Func<TSource, int?> selector) { }
    public double? Average(Func<TSource, long?> selector) { }
    public float? Average(Func<TSource, float?> selector) { }
    public float Average(Func<TSource, float> selector) { }
    public IEnumerable<TSource[]> Chunk(int size) { }
    public bool Contains(TSource value) { }
    public bool Contains(TSource value, IEqualityComparer<TSource>? comparer) { }
    public int Count() { }
    public int Count(Func<TSource, bool> predicate) { }
    public IEnumerable<TSource?> DefaultIfEmpty() { }
    public IEnumerable<TSource> DefaultIfEmpty(TSource defaultValue) { }
    public IEnumerable<TSource> DistinctBy<TKey>(Func<TSource, TKey> keySelector) { }
    public IEnumerable<TSource> DistinctBy<TKey>(Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<TSource> Distinct() { }
    public IEnumerable<TSource> Distinct(IEqualityComparer<TSource>? comparer) { }
    public TSource? ElementAtOrDefault(Index index) { }
    public TSource? ElementAtOrDefault(int index) { }
    public TSource ElementAt(Index index) { }
    public TSource ElementAt(int index) { }
    public TSource? FirstOrDefault() { }
    public TSource FirstOrDefault(TSource defaultValue) { }
    public TSource? FirstOrDefault(Func<TSource, bool> predicate) { }
    public TSource FirstOrDefault(Func<TSource, bool> predicate, TSource defaultValue) { }
    public TSource First() { }
    public TSource First(Func<TSource, bool> predicate) { }
    public IEnumerable<IGrouping<TKey, TSource>> GroupBy<TKey>(Func<TSource, TKey> keySelector) { }
    public IEnumerable<IGrouping<TKey, TSource>> GroupBy<TKey>(Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<IGrouping<TKey, TElement>> GroupBy<TKey, TElement>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector) { }
    public IEnumerable<IGrouping<TKey, TElement>> GroupBy<TKey, TElement>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<TResult> GroupBy<TKey, TResult>(Func<TSource, TKey> keySelector, Func<TKey, IEnumerable<TSource>, TResult> resultSelector) { }
    public IEnumerable<TResult> GroupBy<TKey, TResult>(Func<TSource, TKey> keySelector, Func<TKey, IEnumerable<TSource>, TResult> resultSelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<TResult> GroupBy<TKey, TElement, TResult>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, Func<TKey, IEnumerable<TElement>, TResult> resultSelector) { }
    public IEnumerable<TResult> GroupBy<TKey, TElement, TResult>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, Func<TKey, IEnumerable<TElement>, TResult> resultSelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<(int Index, TSource Item)> Index() { }
    public TSource? LastOrDefault() { }
    public TSource LastOrDefault(TSource defaultValue) { }
    public TSource? LastOrDefault(Func<TSource, bool> predicate) { }
    public TSource LastOrDefault(Func<TSource, bool> predicate, TSource defaultValue) { }
    public TSource Last() { }
    public TSource Last(Func<TSource, bool> predicate) { }
    public long LongCount() { }
    public long LongCount(Func<TSource, bool> predicate) { }
    public TSource? MaxBy<TKey>(Func<TSource, TKey> keySelector) { }
    public TSource? MaxBy<TKey>(Func<TSource, TKey> keySelector, IComparer<TKey>? comparer) { }
    public TSource? Max() { }
    public TSource? Max(IComparer<TSource>? comparer) { }
    public decimal Max(Func<TSource, decimal> selector) { }
    public double Max(Func<TSource, double> selector) { }
    public int Max(Func<TSource, int> selector) { }
    public long Max(Func<TSource, long> selector) { }
    public decimal? Max(Func<TSource, decimal?> selector) { }
    public double? Max(Func<TSource, double?> selector) { }
    public int? Max(Func<TSource, int?> selector) { }
    public long? Max(Func<TSource, long?> selector) { }
    public float? Max(Func<TSource, float?> selector) { }
    public float Max(Func<TSource, float> selector) { }
    public TResult? Max<TResult>(Func<TSource, TResult> selector) { }
    public TSource? MinBy<TKey>(Func<TSource, TKey> keySelector) { }
    public TSource? MinBy<TKey>(Func<TSource, TKey> keySelector, IComparer<TKey>? comparer) { }
    public TSource? Min() { }
    public TSource? Min(IComparer<TSource>? comparer) { }
    public decimal Min(Func<TSource, decimal> selector) { }
    public double Min(Func<TSource, double> selector) { }
    public int Min(Func<TSource, int> selector) { }
    public long Min(Func<TSource, long> selector) { }
    public decimal? Min(Func<TSource, decimal?> selector) { }
    public double? Min(Func<TSource, double?> selector) { }
    public int? Min(Func<TSource, int?> selector) { }
    public long? Min(Func<TSource, long?> selector) { }
    public float? Min(Func<TSource, float?> selector) { }
    public float Min(Func<TSource, float> selector) { }
    public TResult? Min<TResult>(Func<TSource, TResult> selector) { }
    public IOrderedEnumerable<TSource> OrderByDescending<TKey>(Func<TSource, TKey> keySelector) { }
    public IOrderedEnumerable<TSource> OrderByDescending<TKey>(Func<TSource, TKey> keySelector, IComparer<TKey>? comparer) { }
    public IOrderedEnumerable<TSource> OrderBy<TKey>(Func<TSource, TKey> keySelector) { }
    public IOrderedEnumerable<TSource> OrderBy<TKey>(Func<TSource, TKey> keySelector, IComparer<TKey>? comparer) { }
    public IEnumerable<TSource> Prepend(TSource element) { }
    public IEnumerable<TSource> Reverse() { }
    public IEnumerable<TResult> SelectMany<TResult>(Func<TSource, IEnumerable<TResult>> selector) { }
    public IEnumerable<TResult> SelectMany<TResult>(Func<TSource, int, IEnumerable<TResult>> selector) { }
    public IEnumerable<TResult> SelectMany<TCollection, TResult>(Func<TSource, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) { }
    public IEnumerable<TResult> SelectMany<TCollection, TResult>(Func<TSource, int, IEnumerable<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector) { }
    public IEnumerable<TResult> Select<TResult>(Func<TSource, int, TResult> selector) { }
    public IEnumerable<TResult> Select<TResult>(Func<TSource, TResult> selector) { }
    public TSource? SingleOrDefault() { }
    public TSource SingleOrDefault(TSource defaultValue) { }
    public TSource? SingleOrDefault(Func<TSource, bool> predicate) { }
    public TSource SingleOrDefault(Func<TSource, bool> predicate, TSource defaultValue) { }
    public TSource Single() { }
    public TSource Single(Func<TSource, bool> predicate) { }
    public IEnumerable<TSource> SkipLast(int count) { }
    public IEnumerable<TSource> SkipWhile(Func<TSource, bool> predicate) { }
    public IEnumerable<TSource> SkipWhile(Func<TSource, int, bool> predicate) { }
    public IEnumerable<TSource> Skip(int count) { }
    public decimal Sum(Func<TSource, decimal> selector) { }
    public double Sum(Func<TSource, double> selector) { }
    public int Sum(Func<TSource, int> selector) { }
    public long Sum(Func<TSource, long> selector) { }
    public decimal? Sum(Func<TSource, decimal?> selector) { }
    public double? Sum(Func<TSource, double?> selector) { }
    public int? Sum(Func<TSource, int?> selector) { }
    public long? Sum(Func<TSource, long?> selector) { }
    public float? Sum(Func<TSource, float?> selector) { }
    public float Sum(Func<TSource, float> selector) { }
    public IEnumerable<TSource> TakeLast(int count) { }
    public IEnumerable<TSource> TakeWhile(Func<TSource, bool> predicate) { }
    public IEnumerable<TSource> TakeWhile(Func<TSource, int, bool> predicate) { }
    public IEnumerable<TSource> Take(int count) { }
    public IEnumerable<TSource> Take(Range range) { }
    public TSource[] ToArray() { }
    public HashSet<TSource> ToHashSet() { }
    public HashSet<TSource> ToHashSet(IEqualityComparer<TSource>? comparer) { }
    public List<TSource> ToList() { }
    public ILookup<TKey, TSource> ToLookup<TKey>(Func<TSource, TKey> keySelector) { }
    public ILookup<TKey, TSource> ToLookup<TKey>(Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer) { }
    public ILookup<TKey, TElement> ToLookup<TKey, TElement>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector) { }
    public ILookup<TKey, TElement> ToLookup<TKey, TElement>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey>? comparer) { }
    public bool TryGetNonEnumeratedCount(out int count) { }
    public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { }
    public IEnumerable<TSource> Where(Func<TSource, int, bool> predicate) { }
}
public partial extension Enumerable<TSource, TKey>(IEnumerable<TSource> source) where TKey : notnull
{
    public IEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TAccumulate>(Func<TSource, TKey> keySelector, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, IEqualityComparer<TKey>? keyComparer = null) { }
    public IEnumerable<KeyValuePair<TKey, TAccumulate>> AggregateBy<TAccumulate>(Func<TSource, TKey> keySelector, Func<TKey, TAccumulate> seedSelector, Func<TAccumulate, TSource, TAccumulate> func, IEqualityComparer<TKey>? keyComparer = null) { }
    public IEnumerable<KeyValuePair<TKey, int>> CountBy(Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? keyComparer = null) { }
    public Dictionary<TKey, TSource> ToDictionary(Func<TSource, TKey> keySelector) { }
    public Dictionary<TKey, TSource> ToDictionary(Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer) { }
    public Dictionary<TKey, TElement> ToDictionary<TElement>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector) { }
    public Dictionary<TKey, TElement> ToDictionary<TElement>(Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey>? comparer) { }
}
public partial extension Enumerable(IEnumerable<decimal> source)
{
    public decimal Average() { }
    public decimal Max() { }
    public decimal Min() { }
    public decimal Sum() { }
}
public partial extension Enumerable(IEnumerable<double> source)
{
    public double Average() { }
    public double Max() { }
    public double Min() { }
    public double Sum() { }
}
public partial extension Enumerable(IEnumerable<int> source)
{
    public double Average() { }
    public int Max() { }
    public int Min() { }
    public int Sum() { }
}
public partial extension Enumerable(IEnumerable<long> source)
{
    public double Average() { }
    public long Max() { }
    public long Min() { }
    public long Sum() { }
}
public partial extension Enumerable(IEnumerable<decimal?> source)
{
    public decimal? Average() { }
    public decimal? Max() { }
    public decimal? Min() { }
    public decimal? Sum() { }
}
public partial extension Enumerable(IEnumerable<double?> source)
{
    public double? Average() { }
    public double? Max() { }
    public double? Min() { }
    public double? Sum() { }
}
public partial extension Enumerable(IEnumerable<int?> source)
{
    public double? Average() { }
    public int? Max() { }
    public int? Min() { }
    public int? Sum() { }
}
public partial extension Enumerable(IEnumerable<long?> source)
{
    public double? Average() { }
    public long? Max() { }
    public long? Min() { }
    public long? Sum() { }
}
public partial extension Enumerable(IEnumerable<float?> source)
{
    public float? Average() { }
    public float? Max() { }
    public float? Min() { }
    public float? Sum() { }
}
public partial extension Enumerable(IEnumerable<float> source)
{
    public float Average() { }
    public float Max() { }
    public float Min() { }
    public float Sum() { }
}
public partial extension Enumerable(IEnumerable source)
{
    public IEnumerable<TResult> Cast<TResult>() { }
    public IEnumerable<TResult> OfType<TResult>() { }
}
public partial extension Enumerable<TSource>(IEnumerable<TSource> first)
{
    public IEnumerable<TSource> Concat(IEnumerable<TSource> second) { }
    public IEnumerable<TSource> ExceptBy<TKey>(IEnumerable<TKey> second, Func<TSource, TKey> keySelector) { }
    public IEnumerable<TSource> ExceptBy<TKey>(IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<TSource> Except(IEnumerable<TSource> second) { }
    public IEnumerable<TSource> Except(IEnumerable<TSource> second, IEqualityComparer<TSource>? comparer) { }
    public IEnumerable<TSource> IntersectBy<TKey>(IEnumerable<TKey> second, Func<TSource, TKey> keySelector) { }
    public IEnumerable<TSource> IntersectBy<TKey>(IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<TSource> Intersect(IEnumerable<TSource> second) { }
    public IEnumerable<TSource> Intersect(IEnumerable<TSource> second, IEqualityComparer<TSource>? comparer) { }
    public bool SequenceEqual(IEnumerable<TSource> second) { }
    public bool SequenceEqual(IEnumerable<TSource> second, IEqualityComparer<TSource>? comparer) { }
    public IEnumerable<TSource> UnionBy<TKey>(IEnumerable<TSource> second, Func<TSource, TKey> keySelector) { }
    public IEnumerable<TSource> UnionBy<TKey>(IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<TSource> Union(IEnumerable<TSource> second) { }
    public IEnumerable<TSource> Union(IEnumerable<TSource> second, IEqualityComparer<TSource>? comparer) { }
}
public partial extension Enumerable<TOuter>(IEnumerable<TOuter> outer)
{
    public IEnumerable<TResult> GroupJoin<TInner, TKey, TResult>(IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector) { }
    public IEnumerable<TResult> GroupJoin<TInner, TKey, TResult>(IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, IEnumerable<TInner>, TResult> resultSelector, IEqualityComparer<TKey>? comparer) { }
    public IEnumerable<TResult> Join<TInner, TKey, TResult>(IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector) { }
    public IEnumerable<TResult> Join<TInner, TKey, TResult>(IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector, Func<TOuter, TInner, TResult> resultSelector, IEqualityComparer<TKey>? comparer) { }
}
public partial extension Enumerable<T>(IEnumerable<T> source)
{
    public IOrderedEnumerable<T> OrderDescending() { }
    public IOrderedEnumerable<T> OrderDescending(IComparer<T>? comparer) { }
    public IOrderedEnumerable<T> Order() { }
    public IOrderedEnumerable<T> Order(IComparer<T>? comparer) { }
}
public partial extension Enumerable(TSource[] source)
{
    public IEnumerable<TSource> Reverse<TSource>() { }
}
public partial extension Enumerable<TSource>(IOrderedEnumerable<TSource> source)
{
    public IOrderedEnumerable<TSource> ThenByDescending<TKey>(Func<TSource, TKey> keySelector) { }
    public IOrderedEnumerable<TSource> ThenByDescending<TKey>(Func<TSource, TKey> keySelector, IComparer<TKey>? comparer) { }
    public IOrderedEnumerable<TSource> ThenBy<TKey>(Func<TSource, TKey> keySelector) { }
    public IOrderedEnumerable<TSource> ThenBy<TKey>(Func<TSource, TKey> keySelector, IComparer<TKey>? comparer) { }
}
public partial extension Enumerable<TKey, TValue>(IEnumerable<KeyValuePair<TKey, TValue> source)  where TKey : notnull
{
    public Dictionary<TKey, TValue> ToDictionary() { }
    public Dictionary<TKey, TValue> ToDictionary(IEqualityComparer<TKey>? comparer) { }
}
public partial extension Enumerable<TKey, TValue>(IEnumerable<(TKey Key, TValue Value) source) where TKey : notnull
{
    public Dictionary<TKey, TValue> ToDictionary() { }
    public Dictionary<TKey, TValue> ToDictionary(IEqualityComparer<TKey>? comparer) { }
}
public partial extension Enumerable<TFirst>(IEnumerable<TFirst> first)
{
    public IEnumerable<(TFirst First, TSecond Second)> Zip<TSecond>(IEnumerable<TSecond> second) { }
    public IEnumerable<(TFirst First, TSecond Second, TThird Third)> Zip<TSecond, TThird>(IEnumerable<TSecond> second, IEnumerable<TThird> third) { }
    public IEnumerable<TResult> Zip<TSecond, TResult>(IEnumerable<TSecond> second, Func<TFirst, TSecond, TResult> resultSelector) { }
}
public partial extension Enumerable() // no underlying type means traditional static methods
{
    public IEnumerable<TResult> Empty<TResult>() { }
    public IEnumerable<int> Range(int start, int count) { }
    public IEnumerable<TResult> Repeat<TResult>(TResult element, int count) { }!
}

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Nov 16, 2024

@hez2010 your example introduces the type parameters in a way that I didn't think anyone on the team would be comfortable with. The outer extended type now depends on (differently) each internal member. Which may have different things like constraints.

@CyrusNajmabadi
Copy link
Member

and their ramifications?.

We are discussing the ramifications of not supporting something or not. Like if we support user control of "pass by ref or by value".

This also affects things like "is generic inference two phase, or one phase".

The goal is not final, shipping, immutable syntax.

@HaloFour
Copy link
Contributor

@xamir82

Isn't this whole issue centered around discussion about different syntactic styles and their ramifications?

The ramifications being the important bit. Yes, it's a bit of syntactic bikeshedding to see what kind of forms can potentially resolve the concerns by encompassing the functionality of existing extensions and the common patterns around their design today. But the syntax coming out of that doesn't necessarily represent the end state.

@aradalvand
Copy link
Contributor

aradalvand commented Nov 16, 2024

@hez2010 your example doesn't introduce the type parameters anywhere.

@CyrusNajmabadi You can trivially add generic params to @hez2010's example like I showed. I don't understand the criticism.

@hez2010
Copy link

hez2010 commented Nov 16, 2024

your example introduces the type parameters in a way that I didn't think anyone on the team would be comfortable with. The outer extended type now depends on (differently) each internal member. Which may have different things like constraints.

I just forgot about that, now the example has been updated.

@CyrusNajmabadi
Copy link
Member

@CyrusNajmabadi You can trivially add generics params to @hez2010's example like I showed. That's not a substantial criticism.

I brought up our concern on what you showed.

But that isn't relevant RIGHT NOW.

We are not syntactic bike shedding currently. We're defining capabilities and requirements. The semantics of things are clear with our STRAWMAN syntax, which is what we care about.

The STRAWMAN demonstrates that type parameters aren't part of the outer symbol. That they (and their constraints) are scoped to particular symbols. That you can have multiple extended types in the same outer type. That you can mix and match normal statics and static extensions. Etc. etc. etc.

It's a syntax aligned with separation of concerns. It's not a statement that this will be the easy users write these. Or that if it is a way that it is the only way they do it.

@HaloFour
Copy link
Contributor

Feels like a lot more syntax to capture exactly what the extension groups do today. Is it less onerous to have a level of indentation? I don't think so, but that's highly subjective.

Either way, as Cyrus mentioned, we're not at that stage. Sure, all of these forms are possibilities. I wouldn't dwell on exact application of the clause as much as I would how the syntax forms can convey those other features. For example, we all seem to be gravitating towards the use of primary constructor-ish syntax to convey the target parameter, and I think that's an awesome thing as that immediately ticks a lot of checkboxes around compatibility. We also seem to agree that we prefer members themselves use syntax that look very similar to normal instance methods, and their containing scope is what makes then an extension member. I also think that's awesome as it doesn't require a bunch of new syntax to be invented at the member level. These building blocks could be put together any number of ways in the end.

@CyrusNajmabadi
Copy link
Member

your example introduces the type parameters in a way that I didn't think anyone on the team would be comfortable with. The outer extended type now depends on (differently) each internal member. Which may have different things like constraints.

I just forgot about that, now the example has been updated.

The earlier concern I raised about generic declarations not producing generic symbols now applies.

But ignoring that, your approach seems identical to what we have been proposing, just with a syntactic isomorphisms. That's not interesting to discuss as it doesn't change semantics or fundamental capabilities. That's a final bikeshedding pass over what final syntax we think is most pleasant to read, write, and maintain.

We know every design has an infinite number of syntactic isomorphisms. It's part of Navy designs that we don't consider those initially, as we could be spending forever just shuffling deck chairs around.

We care about semantics, capabilities and requirements first and foremost. Bikeshedding syntax comes after we are satisfied with that.

--

Note: we made the mistake of caring any syntax earlier on. And it boxed us into corners where the syntax couldn't solve the needs we had. That's why we don't want to make the mistake of jumping to a final syntax first.

@hez2010
Copy link

hez2010 commented Nov 17, 2024

By the way, why do we want to upgrade those old LINQ methods in the first place? They work pretty well today and will continue to work well. Upgrading those APIs which were designed in a member-wise way to a type-wise extension without breaking change is NOT possible, and no one is asking for the compatibility of upgrading those old extension methods.
What the developers actually want is the final stage where one can extend an interface for a type, which is exactly a type-wise approach.
Let's keep those existing LINQ APIs as is and focus on new capabilities for future API design only.

@HaloFour
Copy link
Contributor

By the way, why do we want to upgrade those old LINQ methods in the first place?

LINQ is just an example of the diversity of the ecosystem of extensions that have been designed in the past 18 years. It represents 18 years of design guidance around API design. All of that isn't going to get thrown out the window. Developers would fully expect to be able to migrate to the new syntax, and to be able to design their extensions side-by-side instead of having to use two completely different mechanisms organized in two completely different ways just to add a new extension member.

What the developers actually want is the final stage where one can extend an interface for a type, which is exactly a type-wise approach.

That's far from the only thing developers want. They've wanted extension properties for a long time, and extension operators as well. Besides, there's nothing about a memberwise approach that precludes extension implementation.

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

No branches or pull requests