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

C# Design Notes for Oct 25 and 26, 2016 #16640

Closed
MadsTorgersen opened this issue Jan 20, 2017 · 10 comments
Closed

C# Design Notes for Oct 25 and 26, 2016 #16640

MadsTorgersen opened this issue Jan 20, 2017 · 10 comments

Comments

@MadsTorgersen
Copy link
Contributor

C# Language Design Notes for Oct 25 and 26, 2016

Agenda

  • Declaration expressions as a generalizing concept
  • Irrefutable patterns and definite assignment
  • Allowing tuple-returning deconstructors
  • Avoiding accidental reuse of out variables
  • Allowing underbar as wildcard character

Declaration expressions

In C# 6.0 we embraced, but eventually abandoned, a very general notion of "declaration expressions" - the idea that a variable could be introduced as an expression int x.

The full generality of that proposal had some problems:

  • A variable introduced like int x is unassigned and therefore unusable in most contexts. It also leads to syntactic ambiguities
  • We therefore allowed an optional initializer int x = e, so that it could be used elsewhere. But that lead to weirdness around when = e meant assignment (the result of which is a value) and when it meant initialization (the result of which is a variable that can be assigned to or passed by ref).

However, there's value in looking at C#'s new deconstruction and out variable features through the lens of declaration expressions.

Currently we have

  • Deconstructing assignments of the form (x, y) = e
  • Deconstructing declarations of the form (X x, Y y) = e
  • Out variables of the form M(out X x)

This calls for generalization. What if we said that

  • Tuple expressions (e1, e2) can be lvalues if all their elements are lvalues, and will cause deconstruction when assigned to
  • There are declaration expressions X x that can only occur in positions where an unassigned variable is allowed, that is
    • In or as part of the left hand side of an assignment
    • In or as part of an out argument

Then all the above features - and more - can be expressed in terms of combinations of the two:

  • Deconstructing assignments are just a tuple on the left hand side of an assignment
  • Deconstructing declarations are just deconstructing assignments where all the nested variables are declaration expressions
  • Out variables are just declaration expressions passed as an out argument

Given the short time left for fixes to C# 7.0 we could still keep it to these three special cases for now. However, if those restrictions were later to be lifted, it would lead to a number of other things being expressible:

  • Using a "deconstructing declaration" as an expression (today it is a statement)
  • Mixing existing and new variables in a deconstructing assignment (x, int y) = e
  • deconstruction in out context M(out (x, y))
  • single declaration expression on the left hand side of assignment int x = e

The work involved in moving to this model without adding this functionality would be to modify the representation in the Roslyn API, so that it can be naturally generalized later.

Conclusion

Let's re-cast the currently planned C# 7.0 features in turns of declaration expressions and tuple expressions, and then plan to later add some or all of the additional expressiveness this enables.

Irrefutable patterns

Some patterns are "irrefutable", meaning that they are known by the compiler to be always fulfilled. We currently make very limited use of this property in the "subsumption" analysis of switch cases. But we could use it elsewhere:

if (GetInt() is int i) { ... }
UseInt(i); // Currently an error. We could know that i is definitely assigned here

This might not seem to be of much value - why are you applying a pattern if it never fails? But in a future with recursive patterns, this may become more useful:

if (input is Assignment(Expression left, var right) && left == right) { ... }
... // condition failed, but left and right are still assigned

Conclusion

This seems harmless, and will grow more useful over time. It's a small tweak that we should do.

Tuple-returning deconstructors

It's a bit irksome that deconstructors must be written with out parameters. This is to allow overloading on arity, but in practice most types would only declare one. We could at least optionally allow one of them to be defined with a return tuple instead:

(int x, int y) Deconstruct() => (X, Y);

Instead of:

void Deconstruct(out int x, out int y) => (x, y) = (X, Y);

Evidence is inconclusive as to which is more efficient: it depends on circumstances.

Conclusion

Not worth it.

Definite assignment for out vars

With the new scope rules, folks have been running into this:

if (int.TryParse(s1, out var i)) { ... i ... }
if (int.TryParse(s2, out var j)) { ... i ... } // copy/paste bug - still reading i instead of j

It works the same as when people had to declare their own variables outside the if, but it seems a bit of a shame that we can't do better now. Could we do something with definite assignment of out variables that could prevent this situation?

Conclusion

Whatever we could do here would be too specific for a language solution. It would be a great idea for a Roslyn analyzer, which can

  • identify TryFoo methods by looking for "Try" in the name and the bool return type
  • find this bug in existing code declaring the variable ahead of time, as well as in new code using out variables

Underbar wunderbar

Can we make _ the wildcard instead of *? We'd need to be sneaky in order to preserve current semantics (_ is a valid identifier) while allowing it as a wildcard in new kinds of code.

M(out var _);
int _ = e.M(_ => ..._...x...); // outer _ is still a named variable?
(int x, var _) = e.M(_ => ..._...x...);
(_, _) = ("x", 2);

We would need to consider rules along the following lines:

  • Make _ always a wildcard in deconstructions and patterns
  • Make _ always a wildcard in declaration expressions and patterns
  • Allow _ as a wildcard in other l-value situations (out arguments at least, but maybe assignment) when it's not already defined
  • Allow _ to be declared more than once in the same scope, in which case it is a wildcard when mentioned
  • Allow _ to be "redeclared" in an inner scope, and then it's a wildcard in the inner scope

"Once a wildcard, always a wildcard!"

Conclusion

This is worth pursuing. More thinking is needed!

@KodrAus
Copy link

KodrAus commented Jan 20, 2017

Something else about a wildcard _ that might be worth thinking about is getting a generic type definition. Right now we do typeof(Dictionary<,>), but maybe we could also do typeof(Dictionary<_, _>). Of course those semantics are subtly different because if you could express typeof(Dictionary<_, _>) then it would be reasonable to do typeof(Dictionary<_, object>), which is not currently supported by typeof.

@HaloFour
Copy link

Getting these notes from the design meetings is great but these feel so out of date compared to the conversations that have already taken place here. I know that you guys are busy but it would be nice to get these in a more timely fashion.

@KodrAus

You can do the following today:

Type temp = typeof(Dictionary<,>);
Type type = temp.MakeGenericType(typeof(string), temp.GetGenericArguments()[1]);

Of course it's a bit messy but that's basically what the compiler would be required to emit as I don't think that you can obtain a type handle to a partially open generic type.

The big question would be what use case you intend to satisfy? Partially open generic types are nasty beasts, moreso than open generic types. The CLR has very little support for doing anything with them.

There might be arguments for using open or partially open generic types in patterns, but you mentioned typeof specifically.

@KodrAus
Copy link

KodrAus commented Jan 20, 2017

@HaloFour I don't really have a usecase, just drawing a loose syntactic parallel for specifying types with unbounded generic values. Maybe that could come into play in the future when matching on types, or inferring them. I haven't given it much thought though.

@dsaf
Copy link

dsaf commented Jan 20, 2017

Could we do something with definite assignment of out variables that could prevent this situation?

I think the members of community have raised their concerns about this in past #12597 , #12939. Perhaps it's worth reverting the late scoping changes?

It works the same as when people had to declare their own variables outside the if...

Not sure if this is the case as when something is declared outside it is a strong indication that it is intended to be used outside by the developer.

@Thaina
Copy link

Thaina commented Jan 20, 2017

@HaloFour is it valid syntax to fill only some generic?

Such as Dictionary<string,>

@DavidArno
Copy link

DavidArno commented Jan 20, 2017

Re: Tuple-returning deconstructors
It is indeed a bit irksome (I'd put it way stronger though) that deconstructors must be written with out parameters. Your focus on efficiency is depressing. A good principle principle to follow though is "make it readable above all else; only degrade readability if efficiency is a real issue". So focusing on efficiency from the outset seems wrong. Further as there's no suggestion of removing the out parameter version support, it also seems moot. A deconstructor using a tuple is more readable and thus there's benefit to it. So your conclusion that it's not worth it is an invalid conclusion, in my view.

Re: Definite assignment for out vars
Whatever we could do here would be too specific for a language solution is not correct. There is a clear, non-specific, solution here: reverse the decision to allow out vars to leak into the surrounding scope.

However, if you won't do that, providing work-arounds via analysers is probably the best course of action.

Re: Irrefutable patterns
One definite candidate for an irrefutable pattern is is var, as discussed in #15489. Whilst not a bug in the sense of the C# 7 language spec, the else could be treated as unreachable under any irrefutable pattern rules.

@Thaina
Copy link

Thaina commented Jan 20, 2017

I wish we could return immutable reference, nullable reference and tuple reference so we could drop out function without performance problem

@GeirGrusom
Copy link

Can we make _ the wildcard instead of *?

What is the actual reason for wanting this in the first place? It seems like a change that requires a lot of work and sneakiness, and what exactly is the benefit?

@MadsTorgersen
Copy link
Contributor Author

@HaloFour Apologies for posting the notes late. I am catching up after a very hectic time. There are a few more in the pipeline. As you say, most of this has been communicated through other channels already - the main reason to still post them is so that the rationale and extra detail is captured.

@GeirGrusom The reason for the shift to _ is overwhelming user feedback and our own sense that it reads better. For feedback, just see the .NET blog post comments, for instance. We had convinced ourselves that it was not possible to use _, and we gave it a rethink and found a way. It is sneaky yes, but we are convinced that it is worth it, and won't lead to too many surprises.

There are more details on wildcards in #16674, which I just posted - including a name change to "discards".

@jcouv
Copy link
Member

jcouv commented Jul 29, 2017

LDM notes for Oct 25/26 2016 are available at https://github.com/dotnet/csharplang/blob/master/meetings/2016/LDM-2016-10-25-26.md
I'll close the present issue. Thanks

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

8 participants