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

Champion: using aliases for any types #4284

Open
3 of 4 tasks
agocke opened this issue Dec 29, 2020 · 27 comments
Open
3 of 4 tasks

Champion: using aliases for any types #4284

agocke opened this issue Dec 29, 2020 · 27 comments

Comments

@agocke
Copy link
Member

agocke commented Dec 29, 2020

Summary

Simply put,

using T = System.Collections.Generic.List<(string, int)>;

is legal. While

using T = (string, int);

is not. I suspect this was simply an oversight, but either way there seems no good reason that it shouldn't work (and it parses terribly and does not produce a good error message either).

Motivation

Language consistency, completing things we started. Without this syntax you cannot give tuple names either, which is a big downside.

Language Change

The grammar will be changed like so:

using_alias_directive
-    : 'using' identifier '=' namespace_or_type_name ';'
+    : 'using' 'unsafe'? identifier '=' namespace_or_type';'
    ;

Concluded Questions

  1. Similarly, what are the semantics of a pointer type in an alias. e.g. using XPtr = X*;. Using aliases are in a location that generally cannot be marked unsafe. So we'd likely want this to be legal, with an error if you then use XPtr in an safe context.

    Answer:
    Aliases to pointers are allowed. However, they must be written in the form using unsafe X = T*;

    1. Using a pointer type in an alias not marked with unsafe will be an error.
    2. It will be an error if using unsafe X = T*; is written and T is not a type that is valid to take a pointer to. Regardless if 'X' is not referenced anywhere in the code.
    3. It will be an error if using unsafe X = T*; would be ok, but the user has not passed the /unsafe flag to the compiler.
  2. What are the semantics of a NRT nullable type with an alias. e.g. using X = string?;. Specifically, does the alias have to be an a #nullable enable region? Or can it be located anywhere. Then, what does it mean if that alias is used in a context that doesn't allow nullable? Is that an error, a warning, or is the nullable annotation silently ignored?

    Answer:

    1. It is an error to have an alias to a top-level NRT type. e.g. using X = string?; is not legal.
    2. using X = List<string?>; remains legal as there is no top-level NRT, just an interior NRT.
    3. using X = int?; remains legal as this is a top-level value type, not a reference type.
  3. For unsafe code, should the syntax be using unsafe X = int*; or unsafe using X = int*;. i.e. does the unsafe modifier come before or after the using keyword.

    Answer (LDM meeting 2/1/23):

    1. The syntax will be using unsafe .... This keeps using blocks consistent with using always being the first token.

LDM Discussions

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-09-20.md#type-alias-improvements
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-08-31.md#using-aliases-for-any-type
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#ungrouped
https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-01-11.md#using-aliases-for-any-types

@CyrusNajmabadi
Copy link
Member

I'll dual champion this :-)

Tbh, I'm fine with any proposal of the form: using id = type_syntax;

@CyrusNajmabadi
Copy link
Member

I'm also willing to do the impl work here. I can think of some nice ways to do it that would be easy to add.

@333fred 333fred closed this as completed Dec 29, 2020
@agocke
Copy link
Member Author

agocke commented Dec 29, 2020

Not quite following how the other issue subsumes this one -- is it purely because the proposed syntactic alterations include namespace-or-type-name on the RHS?

@CyrusNajmabadi
Copy link
Member

I would prefer we break out this narrower proposal. The linked proposal is very broad and contains extra things i don't think are necessary to solve the core problem.

@333fred
Copy link
Member

333fred commented Dec 29, 2020

The linked proposal is very broad and contains extra things i don't think are necessary to solve the core problem.

Personally, my read of the opinion the last time we talked about this was that we're not sure how far we want to go. The proposal itself was relatively simple: permit more things in using aliases. This includes generic types, keywords, and tuples (@agocke this is the part that subsumes this proposal). I wouldn't want to consider a narrower proposal without considering the broader one at the same time.

@CyrusNajmabadi
Copy link
Member

I'm going to reactivate this. POtentially bringing back to LDM to discuss. This is a narrower slice than the braoder topic we originally talked about. Importantly, this is extremely cheap to implement and solves a broad swath of the cases, without having to go into much more complex areas like using X<T> = List<T>;.

@CyrusNajmabadi CyrusNajmabadi self-assigned this Jan 6, 2021
@333fred 333fred added this to the Working Set milestone Feb 11, 2021
@CyrusNajmabadi CyrusNajmabadi changed the title Champion: using aliases for tuple syntax Champion: using aliases for any types Sep 10, 2021
@CyrusNajmabadi
Copy link
Member

Prelim LDM notes:

  1. we should treat the aliases in a semantic (not syntactic) fashion. That means we should not think of the alias as a literal copy/paste of the RHS to the use site. So if you had using X = string? then it should be fine to use X in a #nullable disable region. It would behave the same way that it's legal to have using X = List<string?> and then use that in #nullable disable.

@daiplusplus
Copy link

daiplusplus commented Sep 22, 2021

@CyrusNajmabadi does this include aliases for "open" generic types, e.g.

// Straightforward:
using Foo<T> = List<T>;

// Value-tuple:
using Bar<T> = ( String a, Int32 b, T c );

or

// Straightforward:
using MultiDict<TItem> = IReadOnlyDictionary<Int32,IReadOnlyList<TItem>>;

// Value-tuple:
using MultiDict2<TItem> = IReadOnlyDictionary<Int32,IReadOnlyList<(String a, TItem b)>>;

@CyrusNajmabadi
Copy link
Member

@Jehoel It does not.

@naine
Copy link

naine commented Sep 22, 2021

@Jehoel There is a separate proposal for exactly that: #1239

@NetMage
Copy link

NetMage commented Jun 7, 2022

Based on the proposal, would anonymous types also be allowed?

(I don't see where (value) tuples are in the standard to define type.)

E.g. allow replacing:

var sampleAnonLR = new { left = default(TLeft), rightg = default(IEnumerable<TRight>) };

And then referencing sampleANonLR.GetType() with

using TAnonLR = new { TLeft left, IEnumerable<TRight> rightg };

And then referencing typeof(TAnonLR)

I guess there is no way to write an anonymous type as a type as oppose to a value...

@CyrusNajmabadi
Copy link
Member

No, the proposal as written would not allow for this. Anonymous types have no user expressible name/syntax taht you can use to refer to their type.

@daiplusplus
Copy link

@CyrusNajmabadi Anonymous types have no user expressible name/syntax that you can use to refer to their type.

...which is kinda silly now in 2022: It's a shame that Anonymous Types were originally C# 2.0 to simplify code and allow C# users to be more expressive without the ceremony of fleshed-out class type definitions, but because the design of ATs (and their atrocious ergonomics) seemingly haven't been touched since 2005 it means that any time-savings of using ATs are eliminated by the fact you need to revert back to using ceremonious class definitions if you want to do anything useful with ATs:

  • Implement an interface.
  • Participate in inheritance or composition.
  • Target them for compile-time modification (Fody, etc).
  • Return them from a function.
  • Allow mutable properties.

I do understand why there were these restrictions back in C# 2.0: insufficient dev-time budget for release-day improvements, "less is more", and wanting to see how C# programmers would end-up using anonymous-types to see if further investment in improvements is worthwhile.

While it's now clear that C# 4.0's:Tuple<>, and C# 7.0's ValueTuple<> and C# 9.0's record class types are all simply better at being low-ceremony types compared to ATs, unfortunately there's plenty of showstoppers:

I made a quick comparison table of the main different kinds of product-type/record/tuple in C# today, which I think demonstrates the gap that ATs fall into:

C# Version Definition tedium Has "real" named properties EF/Core Linq projection Exportable Implement interface Custom ctors and class invariants
Mutable class 1.0 Medium Yes Yes Yes Yes Yes, but not with EF: must use mutable properties in projections.
Immutable class 1.0 High Yes Not supported Yes Yes Yes, but not supported by EF at all.
Anonymous Type 2.0 Low Yes Yes No No No
System.Tuple<> 4.0 Low No Not supported Yes No No
System.ValueTuple<> 7.0 Low No. Member names set with attributes. Unsafe. Not supported Yes No No
record class 8.0 Low Yes, but no camelCasing of param names Not supported Yes Yes Yes, but this is surprisingly difficult, and often requires long-form ctor definitions, which eliminates one of record types' main ergonomic advantages: terseness.
  • Footnotes:
    • EF Core requires ATs or POCOs for projections to non-entity types: it explicitly does not support record class types for reasons that make no sense to me (why can't they use Object.ReferenceEquals and just disregard override Object.Equals?)
    • I make it a point to remind people that constructors in OOP are exactly how you implement class invariants.
      • (For background): Invariants (preconditions, postconditions, etc) allow you to make hard guarantees about object-state. That is, if class Foo has a ctor that enforces invariants (e.g. by throwing during construction if there's a parameter validation error) then functions with a Foo-typed parameter can safely depend on those invariants - and this doesn't require full immutability either - overall it makes for much easier-to-understand software with fewer bugs caused by human misunderstandings.
      • ...so it's very frustrating to me that the C# language doesn't make it easy to define or use custom constructors: so much of C# and its ecosystem doesn't seem to have a problem with default-constructors that leave new object instances in an invalid-state which must be manually late-initialized somehow (I'm looking at you WinForms and WPF, the new() generic constraint, Entity Framework, and others), even record types in C# 9.0 are surprisingly difficult to add constructor parameter validation to, despite obvious massive utility.

So looking at the table above, every product-type-kind in C# has its own deal-killers: some of which are unavoidable (e.g. the lack of safety with ValueTuple if you get careless with member names), but some current deal-killers, as listed above, certainly (in my opinion) can and should be rectified somehow - so without getting too side-tracked from this thread's original topic of using aliases, I'd like to suggest:

  • Biggest positive impact: Force the EF Core team to support record types in query projections (and ideally ValueTuple types too).
    • This will eliminate probably the single main use-case for ATs today, paving the way for the feature to be deprecated or even removed in future, as ATs should be entirely redundant given we now have record types and ValueTuple.
  • If the EF Core team remains intransigent then for the next biggest positive impact: make ATs optionally named types and participate in partial class definitions, which in-turn means we can finally make ATs exportable (i.e. public or protected) and have ATs implement interfaces and participate in inheritance and composition.
    • ...and making ATs optionally named is what @NetMage's reply is suggesting - and while I support the idea, I'm not yet sold on the using x = new { ... } syntax.
    • Of course, doing this will just mean that ATs will become just a worse record-type, with zero advantages besides exclusive EF support - but would at least represent a forward step towards hopefully eventually removing ATs?

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Jun 7, 2022

@Jehoel this appears to be out of scope for this proposal. This proposal is about allowing a using alias to any existing type you can already express in C#. You cannot express anonymous-types in c#, so that's not something covered here. If you'd like that to be possible, please open another issue to track that. If it happens, it would fall out that it would then be supported in an alias.

that said... teh point of anonymous types is to be... anonymous. If you're going to give them a name, just write a simple record and you'll be good :)

So looking at the table above, every product-type-kind in C# has its own deal-killers:

Sure. that's why we have options. There is no goal in the language, or surrounding ecosystem for one perfect type that solves all needs for all users in all cases. Pick what makes sense per domain :)

Biggest positive impact: Force the EF Core team to support record types in query projections (and ideally ValueTuple types too).

This is the repo for the C# language. We are not teh EF core team, nor do we force anyone anywhere to do anything. If you would like another team to make a particular change, you can ask them and they can make an appropriate decision for their domain if that's appropriate. It's not our place, or desire, or ability, to force anything.

then for the next biggest positive impact: make ATs ...

Please open tracking discussions on any ideas you have around anonymous types. This issue is not the right place for that discussion. If there is enough interest from the community or an LDM member, it may happen. However, writing off topic posts on unrelated issues is not appropriate and will not change anything here for that other feature.

@daiplusplus

This comment was marked as off-topic.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Jun 7, 2022

But my point is that we can't use record types as a replacement for ATs currently.

Please open discussions on anything related to that. that is unrelated to the goal of allowing a using alias to have any current C# type on the RHS of hte alias.

but at the same time, you can make a lot of people less unhappy with fewer artificial restrictions.

Again, the process for getting changes into the language is to open discussions on things like you'd like to see changed. We prioritize based on value and feedback. Complaining on random other proposals does not change this.

I don't think it's fair to describe my post as off-topic: I was expanding on @NetMage's on-topic suggestion and contributing alternatives.

Netmage asked a question if this was on topic or not. I stated it wasn't. It's fine to ask. It's not fine to continue on this :)

(Ideally GitHub issues would be like GitHub Discussions with tiered/threaded conversations where I could reply to other people without seeming off-topic).

No, our issues are for tracking the actual course of a proposal to implementation (please see our repo guidelines), they are not for free-form conversation about different topics. Please follow our rules while you're here, i imagine you'd like peopel to do the same in your repos :)

@CosminSontu
Copy link

CosminSontu commented Apr 2, 2023

Does this feature intend that the type alias is persistent (still valid when accessed from another namespace)?
What I mean is ... If I have :

namespace A { 
using Unit = System.ValueTuple;
}

and in another file I have:

namespace B {
using namespace A;
// currently, Unit is not a thing in this scope... I need to repeat the same using statement _using Unit = System.ValueTuple_
}

Also, currently the documentation states that when you alias with using, the type names are interchangeable (Unit / System.ValueTuple).
Now If I write specialized behaviour for Unit (ex: extension method), it is also applicable to System.ValueTuple, and this is something I don't want.

Currently the only way to get this outcome (for both cases above) is to derive:
public struct Unit : System.ValueTuple {}

Is there a different proposal if this one does not address these ?
Thanks!

@HaloFour
Copy link
Contributor

HaloFour commented Apr 2, 2023

@CosminSontu

Does this feature intend that the type alias is persistent (still valid when accessed from another namespace)?

No, this proposal does not change the scope in which aliases are defined.

If you want an alias to be defined across an entire project you can use global aliases.

@soroshsabz
Copy link

soroshsabz commented Jun 4, 2023

ITNOA

@CyrusNajmabadi Is this proposal, allow use using alias in another using alias in same scope? (as you can see in #3873) for example

using MyType = List<int>;
using MyDictionary = Dictionary<int, MyType>;

thanks

@CyrusNajmabadi
Copy link
Member

No. It is not.

@soroshsabz
Copy link

@CyrusNajmabadi So I think it is good to adding allowing this in this proposal, because when I can using aliases for anything, so I can using aliases for another using aliases.

And it is meaningless when using alias aliasing the type, I cannot using the alias type in every where that I can use type.

@CyrusNajmabadi
Copy link
Member

@soroshsabz this feature is already complete. We are not intending on changing the design here. If you'd like a new set of features, please open a discussion on that topic.

To be clear, this feature was simply about expanding the set of types allowed on the RHS of an alias to all types. That's it. :) Anything else is outside of the scope of it.

I cannot using the alias type in every where that I can use type.

You def can. For example:

using X = Whatever;
namespace N
{
    using Y = X[]; // can reference 'X' here.
}

It's simply that within a particular set of usings, the aliases defined in that set are not available within that set itself. But that's no issue as you can just provide the exanded form. For example:

using X = Whatever;
using Y = Whatever[];

You may have some duplication in the using-block, but you can still use those aliases interchangeably at the use sites. Given that you only need to declare once, but might reference any number of times, this is totally reasonable.

@soroshsabz
Copy link

soroshsabz commented Jun 5, 2023

@CyrusNajmabadi thanks for details response, the problem is, we cannot use global using into namespace in correctly, so when I have global using Foo = List<int>; I cannot using global using Bar = Dictionary<string, Foo>.

@CyrusNajmabadi
Copy link
Member

Right. But that's fine. Just write:

global using Bar = Dictionary<string, List<int>>. It's the exact same thing.

@Eli-Black-Work
Copy link

@soroshsabz, I think you have a good point, but you should probably start a new discussion or new issue about it, since this issue is feature-frozen 🙂

@jcouv jcouv modified the milestones: Working Set, 12.0 Aug 8, 2023
@soroshsabz
Copy link

@Eli-Black-Work I add new discussion in #7253

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Tracking: Julien
Awaiting triage
Development

No branches or pull requests