meetings/working-groups/extensions/compat-mode-in-extensions.md
This document is very much work in progress!
There are several ways in which the new extension members design causes potential compat breaks, in the sense that a classic extension method being upgraded to the new syntax might break existing callers. The "compat mode" is an attempt at addressing these.
Compat mode is activated by using the this modifier on a receiver parameter:
public static class Enumerable
{
extension<TSource>(this IEnumerable<TSource> source) // `this` means compat mode
{
...
}
}
A design goal of compat mode is that any instance extension method within the extension declaration either a) behaves in a fully compatible way with the corresponding classic extension method, or b) yields a compile time error if that is not possible.
Thus, if an existing classic extension method is ported to the new syntax with compat mode on, it will either be disallowed or guaranteed to continue to work as before.
The following are areas where classic and new extension methods are known to differ. Compat mode addresses each of those differences. More may be added as we discover them. There are currently no examples of giving errors.
Classic extension methods are static methods on the enclosing static class, and they may be invoked as such. In compat mode, a static method is generated by the compiler, using:
public static class Enumerable
{
extension<TSource>(this IEnumerable<TSource> source) // Generate compatible extension methods
{
public IEnumerable<TSource> Where(Func<TSource, bool> predicate) { ... }
public IEnumerable<TSource> Select<TResult>(Func<TSource, TResult> selector) { ... }
}
}
Generates
public static class Enumerable
{
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { ... }
public static IEnumerable<TSource> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) { ... }
}
When type arguments are explicitly given to a classic extension method, they must correspond to all type parameters, including those that are used in the receiver type. By contrast, with new extensions any type parameters on the extension declaration are inferred from the receiver, and type arguments on invocation correspond only to those declared on the extension method itself.
In compat mode, type argument lists are first matched against the full type parameter list generated in the corresponding static method. Only if no applicable such methods are found is an attempt made using only the type parameter list from the extension method itself.
Given:
public static class Enumerable
{
extension<TSource>(this IEnumerable<TSource> source)
{
public IEnumerable<TSource> Select<TResult>(Func<TSource, TResult> selector) { ... }
}
The call myList.Select<int, string>(...) would provide type arguments for TSource and TResult, foregoing the separate inference of TSource from the type of myList.
The call myList.Select<string>(...) would still be allowed, and if it isn't resolved to another extension method in the first round, would provide a type argument for TResult in the above Select method, with TSource being inferred from the type of myList.
When type arguments are inferred for a given classic extension method, any argument may impact the inference of any type parameter. By contrast, with new extensions type arguments for the extension declaration type parameters are inferred from the receiver, whereas arguments to the extension method may only impact type arguments for the extension method's own type parameters.
In compat mode, type arguments are inferred using the generated static method signature, passing the receiver as the first argument.
void M(I<string> i, out object o)
{
i.M1(out o); // infers E.M1<object>
i.M2(out o); // error CS1503: Argument 1: cannot convert from 'out object' to 'out string'
i.M3(out o); // infers E.M3<object>
}
public static class E
{
public static void M1<T>(this I<T> i, out T t) { ... }
extension<T>(I<T> i)
{
public void M2(out T t) { ... }
}
extension<T>(this I<T> i) // Compat mode
{
public void M3(out T t) { ... }
}
}
public interface I<out T> { }
New extension declarations have an inferrability requirement which dictates that any type parameter on the extension declaration occurs at least once in the receiver type. This allows type arguments for those type parameters to be inferred solely from the type of the receiver. However, this prevents certain classic extension methods from being ported to the new syntax, if their type parameters are not in the right order to have those that occur in the receiver type come first in the type parameter list.
In compat mode, the inferrability requirement is waived. This means that type parameters can occur on the extension declaration itself without occurring in the receiver type. For any classic extension method there is therefore the option of porting it to an extension declaration that contains all its type parameters (or as many as needed), instead of leaving them on the extension method declaration.
Making use of this option to have uninferrable type arguments on the extension declaration does introduce other limitations:
public static class E
{
public static IEnumerable<TResult> Select1<TResult, TSource>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) { ... }
extension<TResult, TSource>(this IEnumerable<TSource> source) // TResult not used
{
public static IEnumerable<TResult> Select2(Func<TSource, TResult> selector) { ... }
}
}
Some of the above effects of compat mode might be acceptable for all extension declarations, as they would have little negative effect on the user experience as seen from the perspective of the new extension feature. Perhaps a decent compromise and simplification would be to not have an explicit compat mode, adopt some of its behaviors for all extension members and live with the hopefully small breaking changes caused by abandoning the rest of those behaviors.
Based on currently known scenarios, the two first features - calling extension methods as static methods and passing all type arguments to extension methods - seem likely to be common. Both seem reasonable to support for all new extension methods, as long as the ability to pass only the method's own type arguments is also preserved.
Allowing extension method arguments to influence inference of extension declaration level type arguments seems like a much more contrived scenario. From a user perspective it seems reasonable that we could quietly support it, although the implementation cost - or the degree of complification to the spec - may not be so reasonable. However, even if we give the advice to keep the extension method in classic form, we then have to make two different type inference approaches - the old and the new - coexist side by side and figure out how to make them work together.
Having type parameters "out of order" so that ones that are not used in the this-parameter precede ones that are also seems vanishingly rare. We can probably live with not having those be portable to the new syntax.