Skip to content

Proposal: Allow otherwise unresolvable simple names to resolve to a generic type of the same name with inferred type parameters #8214

Closed
@alrz

Description

@alrz

I want to propose a name resolution for generic types similar to F#'s, plus a relocation extension which I will discuss further.

Usually a separate non-generic class is solely exists to provide static factory methods for a generic type. With this feature there would be no need for that anymore and we can use the same class to define static factory methods and use it while we take advantage of type inference.

Consider this class:

class A<T> {
    public static void M() { }
    public static void M(T arg) { }
    public static void M<U>() { }
    public static void M<U>(U arg) { }
    public static void M<U>(T arg) { }
    public static void M<U>(T arg, U arg1) { }
    public static void M<U, V>() { }
    public static U F<U>(T arg) { }
    public static A<U> G<U>(T arg) { }

}

To clear up when we choose a generic type with a simple name, here's the procedure:

  1. If a non-generic type with the name A exists in the scope we choose that. OK.
  2. If multiple generic types with the name A exist in the scope we need diamond syntax to disambiguate. Fail.
  3. If a single generic type with the name A exists in the scope we choose that. OK.

Note: Using diamond syntax to disambiguate generic types is discussed over at #2319.

This is current syntax:

A<int>.M();          // 1st
A<int>.M(1);         // 2nd    
A<int>.M<int>();     // 3rd
A<int>.M(1.0);       // 4th
A<int>.M<double>(1); // 5th
A<int>.M(1, 2);      // 6th
A<int>.M<int,int>(); // 7th
A<int>.M<int>(1);    // ambiguous between 4th and 5th

And proposed one:

A.M<int>();          // 1st -- relocation
A.M(1);              // 2nd
A.M<int;int>();      // 3rd -- relocation
A.M<int>(1.0);       // 4th -- relocation
A.M<double>(1);      // 5th
A.M(1, 2);           // 6th
A.M<int;int,int>();  // 7th -- relocation
A.M<int>(1);         // ambiguous between 4th and 5th

Relocation means that we specify the type's type arguments in method type arguments location, delimited with a semicolon. When we go down to nested types, we will also use semicolon to separate each type's type arguments.

This is specifically useful together with #952, #6739 and type invocation (#206):

enum class Option<T> {
    Some(T),
    None
}

var option = Some(5);        // type invocation
Option<int> option = None(); // by target
var option = None<int>();    // relocation

This feature can be used with others like #6207 and #5429:

var result = A<int>.F<int>(5);  // current syntax
var result = A.F<U:int>(1);     // selective    
int result = A.F<int>(1);       // relocation, by target
A<int> result = A.G(5);         // by target

One other use case is in using directives. Currently we can define type aliases for any non-generic type like using Callback = System.Action; and with #3993 also for generics using Cache<T> = Dictionary<string, T>;. But we can't use using for concrete types like:

using System.Action;

And if we could, there was no way to express generic types. Java doesn't have this problem because of type erasure.

So assuming Option<T> type above we can write:

using My.Namespace.Option;

To simply bring that single type into the scope. Obviously, if there was a non-generic type named Option either we can keep it or disambiguate it with diamond syntax.

Insignificant type parameters: If the type has no default generic type parameter (#6248) we can take advantage of this feature in methods e.g.

void F(A a) { }
// translates to
void F<T>(A<T> a) { }

Related: #1470

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions