Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
alrz opened this issue Jan 27, 2016 · 3 comments

Comments

@alrz
Copy link
Contributor

alrz commented Jan 27, 2016

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

@HaloFour
Copy link

This seems to be overcomplicated, just to avoid writing a non-generic class in order to surface a cleaner API. I don't like the idea of relocating the generic type arguments around. What about nested generic types? The generic type arguments can be the same name in that case (it is a compiler warning). Seems like a recipe for odd generic incantations of mixing names, commas and semicolons.

It's also important to note that non-generic types and generic types of each arity are all distinctly different types according to the CLR and to C#. You mention System.Action, which is a concrete non-generic type. But there is also System.Action1, System.Action2, System.Action3and so on. In that case at least they differ by signature, but there's absolutely nothing about generic types/delegates that requires that. What would happen if you were tousing System.Option` and a non-generic version existed? Or a generic version with an arity of 2?

@alrz
Copy link
Contributor Author

alrz commented Jan 28, 2016

@HaloFour

This seems to be overcomplicated, just to avoid writing a non-generic class in order to surface a cleaner API. I don't like the idea of relocating the generic type arguments around.

This is actually #2319 with a relaxed context. I'd be happy to restrict this only to what F# offers to keep it simple (hence the title) but it seems without relocation it's a lot less useful. For example, without this, even #952 doesn't offer anything to avoid mentioning the parent class and you'd end up with Option<int>.None() and it cannot use type inference at all so also Option<int>.Some(4);.

What about nested generic types?

You can use semicolons to specify nested generic type parameters inward, for example Foo<T,U>.Bar<V> becomes Bar<T,U;V> if Foo is an enum class. This'll work for other nested generic types like Foo<int>.Bar<int>.M<int>() becomes Foo.Bar.M<int;int;int>();. Although, I believe this shouldn't be allowed like in any case, it might get out of hand. I'd be happy to see where it can be problematic to restrict possibilities accordingly.

The generic type arguments can be the same name in that case (it is a compiler warning).

It woudn't be a problem I guess, because semicolon syntax is still positional, however, it'd be a problem for #6207 which in that case you will need to specify all type parameters in turn.

Seems like a recipe for odd generic incantations of mixing names, commas and semicolons.

I doubt if you have that deep nested generic types, unless you code like #4494. For simple cases it woudn't be that much confusing and rather expressive, e.g. None<int>() — you are using relocation but there is no semicolon.

It's also important to note that non-generic types and generic types of each arity are all distinctly different types

I'm exactly addressing this fact, for example, in Java there is no such problem and you're free to use import with generic types.

What would happen if you were to using System.Option and a non-generic version existed? Or a generic version with an arity of 2?

That works for a single non-generic or generic type. If there was more then we're back to the System.Action example. You can choose the desired type using the diamond syntax,

using System.Option<>;
using System.Action<,>;

However, for using now I think it's better to require diamond syntax even if there was a single generic type, otherwise if you add a non-generic type with the same name it'll break. Note that this applies only to using directives, because the point is to bring a specific type in the scope.

@alrz
Copy link
Contributor Author

alrz commented Jan 31, 2016

Closing in favor of #1470.

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

No branches or pull requests

4 participants