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]: Support generics and generic type parameters in aliases #3993

Closed
HaloFour opened this Issue Jul 17, 2015 · 29 comments

Comments

Projects
None yet
@HaloFour

HaloFour commented Jul 17, 2015

This is an alternative proposal to #116.

I propose the following changes to aliases to support generics:

using-alias-directive:
   using identifier = namespace-or-type-name ;
   using identifier type-parameter-list opt = class-type* ;

* There's probably a better item in the spec for this.

The purpose of this proposal is to allow aliases to be defined for unbound or partially bound generic types where the alias is then treated as a generic type that must be closed at the point at which it is used.

For example the following is legal in C# today:

using IntList = System.Collections.Generic.List<int>;

This expansion would permit the following:

// unbound
using MyList<T> = System.Collections.Generic.List<T>;
// partially bound
using StringDictionary<TValue> = System.Collections.Generic.Dictionary<String, TValue>;

The name of the generic type parameter can be anything. The arguments are matched by their position in the aliased type.

using FooList<FOO> = System.Collections.Generic.List<FOO>;
using ReorderedParameterDictionary<TValue, TKey> = System.Collections.Generic.Dictionary<TKey, TValue>;

When consuming the alias the rules would apply the same as when consuming a generic type:

MyList<int> list = new MyList<int>();
Debug.Assert(list is System.Collections.Generic.List<int>);

StringDictionary<bool> dict = new StringDictionary<bool>();
Debug.Assert(dict is System.Collections.Generic.Dictionary<string, bool>);

ReorderedParameterDictionary<int, string> dict2 = new ReorderedParameterDictionary<int, string>();
Debug.Assert(dict2 is System.Collections.Generic.Dictionary<string, int>);

Arguments:

  1. This syntax cannot be assembled from existing syntax within the specification thus requiring additional work.

    This is true. Even though I provide a syntax above it is likely incorrect and the feature probably requires some new syntax. I believe that this is worth it for the functionality that the feature provides and I also feel that this syntax is more intuitive to the developer than the alternatives.

  2. Requiring the developer to name the generic type arguments is confusing.

    Naming generic type arguments is a very common task for anyone who has to write a generic type or generic method today. This should be somewhat second nature. This proposed syntax is very similar to the same syntax that would be required to define a new generic class with a generic subclass.

  3. Requiring the developer to name the generic type argument requires more keystrokes.

    This would require more keystrokes than simply using the existing unbound type syntax. However, the developer is not bound to using the same generic type parameter names as the type that is being aliased making the required number of additional keystrokes only one per generic type parameter. Given the common convention for generic type parameters is a single character, e.g. T, I don't think that this places an undue burden on the developer defining the alias nor any developer who must read and maintain that alias. It also represent a very small price to pay for the keystroke benefits that the feature would provide.

Complications:

  1. Generic aliasing could lead to recursion, e.g. using MyList<T> = System.Collections.Generic.List<MyList<T>>;.

The rules for aliases today already solves this problem by not permitting an alias to refer to itself or another alias. If that weren't the case this scenario would already be problematic: using MyList = System.Collections.Generic.List<MyList>;
2. The alias can represent a partially open generic type which is problematic if the developer attempts to obtain a Type instance from the alias via typeof().

This is true. Given the StringDictionary<T> alias defined above it would be a problem to attempt to obtain a Type reference using typeof(StringDictionary<>). In my opinion this could be solved in one of two possible ways:

  1. Make it illegal to use the unbound type definition of the alias. The expression typeof(StringDictionary<>) would be a compile-time error, however the expression typeof(StringDictionary<bool>) would be valid and equivalent to typeof(Dictionary<String, bool>). This might be confusing if aliases to an unbound type were supported, e.g. typeof(MyList<>).

  2. Support partially open Type references. This requires a little more code generation but is perfectly legal:

    Type type = typeof(StringDictionary<bool>);
    // equivalent to:
    Type $temp1 = typeof(Dictionary<,>);
    Type[] $temp2 = $temp1.GetGenericArguments();
    Type type = $temp1.MakeGenericType(typeof(string), $temp2[1]);

    I am including this potential solution for the sake of argument. While being able to support syntax typeof(StringDictionary<>) would reduce the confusion for the developer I think that same developer would be more confused if the result is a partially open generic type.

@HaloFour

This comment has been minimized.

HaloFour commented Jul 17, 2015

Alternatively, as an addendum to the syntax proposed by #116 this could be changed to use partially bound type names. If that proposal were implemented this is likely the changes that would need to be made to it in order to add support for partially bound aliases short of having two separate generic alias syntaxes:

using StringDictionary<> = System.Collections.Generic.Dictionary<string,>;
@sharwell

This comment has been minimized.

Member

sharwell commented Jul 17, 2015

I don't see this proposal as an alternative to #116, but rather as a separate feature request intended to solve a different problem -- specifically the ability to use partially-bound generic using directives.

@HaloFour

This comment has been minimized.

HaloFour commented Jul 17, 2015

@sharwell

Perhaps. Given the overlap between the two I feel that it is probably more appropriate to approach them as alternatives. If #116 were to be implemented (or was already implemented) I don't feel that a completely separate syntax such as this would be appropriate and I would instead be pushing for further evolution of your proposal in a similar vein to the first comment I made to this proposal.

@gistofj

This comment has been minimized.

gistofj commented Jul 22, 2015

@HaloFour 👍 I love this. I would use it constantly.

@aluanhaddad

This comment has been minimized.

aluanhaddad commented Jul 28, 2015

👍 This is an excellent idea.

@zippec

This comment has been minimized.

zippec commented Aug 4, 2015

I believe there is a problem with proposed syntax:

using StringDictionary<TValue> = System.Collections.Generic.Dictionary<String, TValue>;

because there is no way for the compiler (other than convention) to tell if it should look up type TValue or if it is unbound. Thus the syntax in #116 is an obvious choice:

using StringDictionary<> = System.Collections.Generic.Dictionary<string,>;

EDIT: I take that back. Of course the compiler can match generic parameter type name from alias to generic parameter type name of aliased type. This will even allow reordering of type parameters.

using MyDictionary<TValue, TKey> = System.Collections.Generic.Dictionary<TKey, TValue>;
@HaloFour

This comment has been minimized.

HaloFour commented Aug 4, 2015

@jveselka

I had used TValue as the name of the generic type parameter just to keep with convention. The name should be anything and the type arguments matched by position. I'll update the proposal to make that more clear. Thanks.

@sharwell

This comment has been minimized.

Member

sharwell commented Aug 4, 2015

This proposed syntax is very similar to the same syntax that would be required to define a new generic class with a generic subclass.

But the proposed syntax is a reference to an existing type, not the declaration of a new type. In C#, the only way to reference an open generic type in code is by omitting the type arguments (e.g. typeof(Dictionary<,>)). This proposal increases developer confusion surrounding an already poorly understood feature by creating a second way to do the same thing.¹

By defining the support for aliasing open generic types using the same syntax as typeof, we not only address this functionality in the simplest possible form, but we also improve developer familiarity and understanding of the prior feature.

¹ I'm ignoring XML documentation comments here, which have their own syntax.

@HaloFour

This comment has been minimized.

HaloFour commented Aug 4, 2015

@sharwell

Why does aliasing need to be artificially limited by being intrinsically tied to typeof? You don't use typeof when defining the alias in any situation, let alone with generic types. The fact that the syntax smells like that used with typeof is not relevant, they don't refer to the same grammars in the specification, accidentally or intentionally. You can't alias void, but you can typeof(void). I see no reason whatsoever to conflate the two. Doing so would only increase developer confusion while simultaneously neutering the functionality of aliasing generic types.

This proposal is simply more intuitive and more functional. It solves the common use case of aliasing partially open generic types such as Dictionary<string,> that cannot be defined through typeof or your proposal. It doesn't have a byproduct of educating the developers about typeof, and it shouldn't.

We're simply going to have to agree to disagree. That is why I submitted a separate proposal.

@paulomorgado

This comment has been minimized.

paulomorgado commented Aug 4, 2015

Given:

using StringDictionary<TValue> = System.Collections.Generic.Dictionary<String, TValue>;

What is typeof(StringDictionary) supposed to be?

@HaloFour

This comment has been minimized.

HaloFour commented Aug 4, 2015

@paulomorgado

typeof(StringDictionary) would be a compiler error since StringDictionary does not exist.

However, typeof(StringDictionary<>) is a different story and one that I explicitly mention in the complications section of the proposal. I put out two potential solutions for the sake of argument, but I am definitely in the first camp where the expression typeof(StringDictionary<>) results in a compiler error.

@TyreeJackson

This comment has been minimized.

TyreeJackson commented Aug 6, 2015

👍 I also agree that this is an excellent idea. I've often wanted to do precisely what this proposal asks for.

@alrz

This comment has been minimized.

Contributor

alrz commented Dec 4, 2015

There is no way to get this out of file scope?

@aluanhaddad

This comment has been minimized.

aluanhaddad commented Dec 4, 2015

@alrz The scope would, l imagine, be consistent with other using directives.

@alrz

This comment has been minimized.

Contributor

alrz commented Dec 5, 2015

I mean something like this,

using class EmailAddress = String;
// or 
class EmailAddress = String;

so it'll be in scope in all files.

@HaloFour

This comment has been minimized.

HaloFour commented Dec 5, 2015

@alrz Sounds like that should be a separate proposal entirely. I imagine it would be a compiler directive, as it is in VB.NET:

/imports:"Foo = System.Int32"

@alrz

This comment has been minimized.

Contributor

alrz commented Dec 5, 2015

@HaloFour I wonder how F# is doing this for type abbreviations like seq<'T>; does it expose these aliases, or seq<'T> is a special case?

@HaloFour

This comment has been minimized.

HaloFour commented Dec 5, 2015

@alrz The is no concept of aliases in the CLR. Every compiler that supports the notion does so in its own fashion and entirely internally. F# seems to treat seq<'T> as an alias for System.Collections.Generic.IEnumerable<T>, which isn't really different from C# treating string as an alias for System.String or VB.NET treating Date as an alias for System.DateTime. A custom type alias in F# isn't encoded in any fashion in the resulting assembly anymore than it is in C# or VB.NET.

@wizzardmr42

This comment has been minimized.

wizzardmr42 commented Jan 3, 2016

@aluanhaddad It would also be useful to take it across assembly boundaries and use it without creating a named alias, neither of which are do-able with aliasing.

@aluanhaddad

This comment has been minimized.

aluanhaddad commented Jan 3, 2016

@wizzardmr42 That sounds like an orthogonal feature.

@wizzardmr42

This comment has been minimized.

wizzardmr42 commented Jan 3, 2016

@aluanhaddad Sorry - just pointing out that wouldn't be a complete solution for everything discussed in #3281, which referenced this

@HaloFour

This comment has been minimized.

HaloFour commented Jan 4, 2016

@wizzardmr42

I was commenting specifically on the mention of the current limitations with type aliases. It was someone else who had suggested that type aliases could be an alternative workaround to the original proposer's problem.

This proposal is only a modest extension of existing aliases in C# and does not touch on the subject of extending them beyond the current code file. @alrz has proposed #7451 to extend aliases throughout the current project using code notation. I personally prefer VB.NET's way of handling that, by using command line arguments to the compiler (exposed through the project configuration) to establish project-wide aliases.

For something beyond the current project there could either be tooling to help configure multiple projects within a solution, or there would need to be a convention of embedding some kind of metadata in the assemblies to declare aliases. That concept seems very messy as it sounds like it would establish a second type system within the compiler that would be erased by compilation and ignored by the CLR.

@wizzardmr42

This comment has been minimized.

wizzardmr42 commented Jan 4, 2016

Sorry - I actually meant to post that on #3281, but it is worth pointing out here too that being able to reference existing types using a "typeof" keyword would solve some of the same problems that this would so may be worth looking at #3281 before implementing this (and #7715 which is a duplicate, but I gave more specific cases where it would be useful there).

@paulomorgado

This comment has been minimized.

paulomorgado commented Jan 4, 2016

@HaloFour,

(...) I personally prefer VB.NET's way of handling that, by using command line arguments to the compiler (exposed through the project configuration) to establish project-wide aliases.

For something beyond the current project there could either be tooling to help configure multiple projects within a solution, or there would need to be a convention of embedding some kind of metadata in the assemblies to declare aliases. That concept seems very messy as it sounds like it would establish a second type system within the compiler that would be erased by compilation and ignored by the CLR. (...)

One immediate benefit of project-wide vs. solution-wide aliases is the possibilty of shared source files binding to different types according to the project they are being compiled against without having them poluted with conditional compiler directives. Atlthough that would erase the obiousness of the per-project semantics.

@alrz

This comment has been minimized.

Contributor

alrz commented Jan 5, 2016

@paulomorgano I didn't think of that! It would be a pleasant workaround for #7451 in case of need for solution-wide aliases.

@TonyValenti

This comment has been minimized.

TonyValenti commented Feb 4, 2016

Hi All,
I was just reading this and this is something that I would love - especially if it had the ability for me to expose the alias into derived classes.

The example that I deal with a lot is related to refactoring generic classes that inherit from other generic classes. Take this example below:

public class BaseClass<T, U> {
    public virtual void DoSomething(T Parameter){

    }
}

public class OriginalParameterType { }
public class RefactoringParameterType { }

public class SpecificImplementation<U> : BaseClass<SpecificParameter, U> {
    public override void DoSomething(SpecificParameter Parameter) {
    }
}

If I want to change SpecificImplementation to inherit from BaseClass<RefactoringParameterType, U>, I then have to dig through the source code (and any other classes that inherit from SpecificImplementation<U>) and change the method's signature to be:

public override void DoSomething(SpecificParameter Parameter) { }

In order to get around that, I'll usually create a type that simply inherits from one of my generic parameters. For example:

public class BaseClass<T, U> {
    public class MyParameterType : T { }

    public virtual void DoSomething(MyParameterType Parameter){

    }
}

public class OriginalParameterType { }
public class RefactoringParameterType { }

//If I replace OriginalParameterType with RefactoringParameterType I don't have to manually refactor all my method definitions!  :-)
public class SpecificImplementation<U> : BaseClass<OriginalParameterType , U> {
    public override void DoSomething(MyParameterType Parameter) {
    }
}

When I do this, all I'm really doing is creating an alias for T that gets propagated down into child classes. In most cases, I really prefer that because I don't have to deal with giant generic definitions and it makes refactoring possible, however, the one downfall of doing things the way that I am is that MyParameterType is not assignable from a <T>.

I love a construct that would allow me to create a true alias for a type and pass that down into derived or other classes.

For example, here is a thought:

public class BaseClass<T, U> {
    //I define the alias here.
    public alias ParameterType: T;

    //Notice that I'm using it here instead of <T>
    public virtual void DoSomething(ParameterType Parameter){

    }
}

public class OriginalParameterType { }

public class SpecificImplementation<U> : BaseClass<OriginalParameterType , U> {
    //And in derived methods, the signature gets exposed the alias: 
    public override void DoSomething(ParameterType Parameter) {
    }

}

....

//And I should even be able to reference the alias in other methods:
var derived = new SpecificImplementation<string>();

//Pull the type off of an instance.
var instance1 = new derived.MyParameterType();

//Pull the type off of the class.
var instance2 = new SpecificImplementation<string>.MyParameterType();
....



@aluanhaddad

This comment has been minimized.

aluanhaddad commented Feb 4, 2016

@TonyValenti While that is certainly a scenario I would like to see supported, it seems like a perfect use case for a custom-refactoring, and not a language feature. This proposal is nicely focused and scoped (no pun intended), adding significant value while impacting little else in the language. That said, I think the scenario you mention is very compelling and I would find it very useful, but it could be a refactoring. That said, if it naturally falls out of this proposal, so much the better.

@dsaf

This comment has been minimized.

dsaf commented Sep 15, 2016

The resemblance is eerie: https://github.com/apple/swift-evolution/blob/master/proposals/0048-generic-typealias.md

typealias DictionaryOfStrings<T : Hashable> = Dictionary<T, String>
using StringDictionary<TValue> = System.Collections.Generic.Dictionary<String, TValue>;
@gafter

This comment has been minimized.

Member

gafter commented Jan 8, 2018

Issue moved to dotnet/csharplang #1239 via ZenHub

@gafter gafter closed this Jan 8, 2018

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