-
Notifications
You must be signed in to change notification settings - Fork 1k
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: Sequence Expressions #377
Comments
It looks like the LDM recently voted down declaration expressions (again). Is this proposal meaningfully different to warrant separate consideration? |
@orthoxerox No, #9 is a PR which integrated the C# language spec. @MgSam I don't see any discussion of declaration expressions in those notes. What are you referring to? |
@MgSam I think that's talking about allowing code like: if (condition)
var foo = bar; |
@svick But that seems pretty clearly useless. I thought it was something like this: var a = foo() ? var b = 5; b + 37 : 0; Which seems like it also fits the definition of sequence expression, no? |
@MgSam That's not what embedded statement means. |
Quite right. My mistake. |
I like this idea. But, why not use braces and An expression-bodied lambda: person => person.Name If we want to execute some statements before returning the name, we can write a statement-bodied lambda: person => { Log("Accessing name"); return person.Name; } If we adopt the parens-based syntax for this proposal then example 2 will have an alternative syntax that accomplishes the exact same thing: person => ( Log("Accessing name"); person.Name ) Which will be confusing, in my opinion. Wouldn't it instead be better to make a code block evaluate to an expression? var name = {
Log("Acessing name");
return person.Name;
} This would then unify the two syntaxes for lambdas. Instead of:
only
would exist. Also, with the proposed syntax, expression-bodied members mean that there are two ways to write any method: public string DoStuff()
{
Log("Doing stuff");
return "some result";
} and public string DoStuff() => (
Log("Doing stuff");
"some result"); Again, with blocks-as-expressions the only thing that changes is the presence of the public string DoStuff() =>
{
Log("Doing stuff");
return "some result";
} I think this results in a more consistent syntax overall. |
That would conflict with existing block syntax since the expression is not at all dependent on an assignment. |
@Richiban you could probably rewrite the grammar to make blocks expressions, but |
@orthoxerox Ahh, you mean that the meaning of string MyMethod()
{
var name = {
Log("Getting name");
return "John";
}
return "Dave";
} and string MyMethod()
{
{
Log("Getting name");
return "John";
}
} |
This syntax would allow a pertty narrow set of statements in sequence expressions. I think we should limit the context in which we can use block expressions to deal with ambiguities instead of limiting statements themselves. I suggest we allow this in assignments, initializers and the switch expression, var x = {
foreach (var item in list) {
if (item == value) {
break true;
}
}
break false;
};
var y = e switch {
true => {
using (r) {
break e.M();
}
},
_ => null
}; which is equivalent to: bool x;
foreach (var item in list) {
if (item == value) {
x = true;
goto _lable;
}
}
x = false;
_label:
object y;
if (e == true) {
using (e) {
y = e.M();
}
} else {
y = null;
} To return the result from a block expression we use PS: we could instead of "sequence expressions" we just introduce if (var x = e; x)
switch (var x = e; x) However, #1090 eliminates double parens even if we had sequence expressions independently. |
What would be the benefit of |
Yes, for while loops this might make sense, but |
This pretty much comes down to "inlining a variable declaration", on the other hand, "block expressions" enable scenarios that is currently not possible with switch expressions, var x = e switch {
true => {
foreach (var item in list) {
return; // return from *method* // not possible with local functions
break; // break the loop
break true; // terminate and give the result to x
}
},
_ => false
} The suggested workaround is to use local functions, var x = e switch {
true => LocalFunction(), ...
}
|
@alrz I like your idea. But for the I think it should forbid |
See #867 which would allow |
@CyrusNajmabadi, how would the following be written today? class C: B
{
public C(int x) :
// shorthand for base(f(x), f(x)) but computing f(x) only once
base( (var t = f(x); t),
t)
{
}
} |
I think this is what people would have to write today: class C: B
{
public C(int x) : base(f(x), 0)
{
}
C(int t, short dummy) : base(t, t)
{
}
} The shorthand does seem convenient for that example. |
I would do what TS does and allow arbitrary code (that does not reference 'this') prior to calling 'base'. Then you get full language/statement access without having to shoehorn everything into expr/arg-list form. The latter feels more like putting the cart before the horse to me. |
Or this: class C : B
{
public C(int x) : base(helper(x, out int y), y) {
}
private static int helper(int x, out int returnValue) {
return returnValue = f(x);
}
} But I'm with Cyrus, lifting the restriction on calling non- |
And for field initializers (e.g. in a record declaration)? Or the loop completion condition in a |
For the base class constructor case, there's #2335 . For the field initializer case: I can't find the exact example in this issue, so I might be misunderstanding badly. If it's for record literals, then I don't think it looks clearer than having temporaries above the field literal. Maybe with #2574 fixed we can something a bit clearer (IMHO): // (1)
var x = (
var blah = blah();
Foo(f(blah), g(blah)
);
// (2)
// instead of
var x = Foo((var blah = blah(); f(blah)), g(blah)); This also fixes my issue with having If it's for record definitions, then: data class Foo {
T0 bar0;
T1 bar1;
}
// or
data class Foo(T0 bar0, T1 bar1);
// I don't know what the latest design for this looks like... Then maybe (throwing things around): data class Foo {
T0 bar0;
T1 bar1;
Foo() {
var blah = blah();
bar0 = f(blah);
bar1 = g(blah);
}
// or
data class Foo(T0 bar0, T1 bar1) {
Foo() {
var blah = blah();
base(f(blah), g(blah)); // maybe something else besides `base`?
// Much better, IMHO, than:
// : base( (var t = f(x); t),
// t)
}
} Here's the catch: Both of those possibilities allow arbitrary code to be run before initialization, even if any particular field will not be using its default value in some instance because it was specified in the literal. But you can do all that with the original proposal, too, otherwise it'd be unsound:
For the |
Seems very unlikely to happen, ever. It is certainly much more complex than sequence expressions. |
That's a bit surprising :( It's not proposing any new functionality. Technically just (a bit unconventional) syntax sugar. This would help in this case: class C: B
{
public C(int x)
: { // Some way to specify you definitely don't mean to run the default `base()`?
var t = f(x);
base(t, t); // `base` can only ever be invoked in the main body of a constructor.
}
} Or, failing that (although I'd really like it...), could we get a spread operator for tuples? class C: B
{
public C(int x) : base(*f_for_c(x)) {}
(T, T) f_for_c(int x) { var t = f(x); return (t, t); }
} It's just that the constructor example here does not seem like the best pitch for sequence expressions. And honestly I'm already down with anything that gets us closer to clean everything's-an-expression. But that requires the scoping rules not to leak names. Those examples I have issues with (all dealing with some form of an ArgumentList and a name leaking from some argument's scope to another's) need the scoping hole. Embracing it would either make #2574 impossible or require some (strange?) rules (like: names leak only in an ArgumentList). |
Why couldn't this syntax be used?
If the last expression in the block doesn't end with a semicolon, then the block is a sequence expression. What else could this be ambiguous with? This could also open up the possibility for if statements to become if expressions.
|
It's not a question of ambiguoty. It's a question of clarity. { Already starts an initializer expression. And having people have to look and see if there's a trailing semicolon or not makes code harder to grok. |
Do you mean an anonymous type initializer? But there is no |
No. I mean normal initializer expressions. There used in a few places. Like: |
But you still have |
I guess there is this And In cases where this syntax could either be an initializer expression or a sequence expression, it could just always be considered an initializer expression. I don't think you would lose anything. If you wanted to force it for seemingly no point you could do |
No. In none of the cases there is a new before the initializer expression. It's just an expression (where other expressions are allowed). You are proposing a new expression that starts with { that would also be legal in all those contexts. That would be very confusing. |
See #3086 for the version of this proposal with braces. |
@gafter commented on Tue Oct 20 2015
We propose a form of expression that allows you to declare temporary variables before the final result of the expression. We replace the existing parenthesized expression with the following
The scope of any variable declared in a parenthesized-expression is the body of that parenthesized-expression.
The type of a parenthesized-expression is the type of its final expression.
At run-time the statements are executed in sequence, and then the final expression is evaluated, which becomes the value of the whole sequence expression.
The definite assignment and reachability rules need to be specified, but they are trivial.
This is particularly useful in combination with the expression form of the pattern matching construct #5154.
/cc @TyOverby
UPDATE 2019-09-11: I no longer think there is a need to so severely restrict which statement forms are permitted, though there are now other issues to consider:
Design Meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#discriminated-unions
@alrz commented on Tue Oct 20 2015
love this 👍
@orthoxerox commented on Tue Oct 20 2015
Why not
?
@alrz commented on Wed Oct 21 2015
@orthoxerox it will be ambiguous in expression-bodied lambdas (discussed at length in #5402).
@gafter commented on Tue Oct 20 2015
@orthoxerox Because that would break compatibility by making
(1+1)
a syntax error for two reasons - first, because1+1
is not one of the expression forms allowed as an expression-statement, and second, because it lacks the semicolon that is part of an expression-statement.Oh, wait, you're suggesting that we replace the parens in a parenthesized expression with curly braces? I think that would make lots of things ambiguous.
@paulomorgado commented on Tue Oct 20 2015
Why is it named sequence?
@gafter commented on Tue Oct 20 2015
@paulomorgado Because it evaluates the expressions sequentially for side effects. Got a better name?
@paulomorgado commented on Tue Oct 20 2015
@gafter, this used to be referred as declaration expressions, right.
Aren't you afraid it might be confused with "as opposed to asynchronous expressions"?
And, no! I don't have a better name. Yet!
@ufcpp commented on Tue Oct 20 2015
What is its result type? That of the last expression? I receive the impression from the name "sequence expression" that its result type is an array of each expression.
@gafter commented on Tue Oct 20 2015
@paulomorgado No, this is not the same as declaration expressions. That was where you could declare a variable at its first use, something like
M(int x = 12, x*x)
.The type and value is the type and value of the last expression.
@paulomorgado commented on Wed Oct 21 2015
See?
@gafter commented on Wed Oct 21 2015
@paulomorgado I'm not sure you understand. This would not be legal under this proposal:
M(int x = 12, x*x)
That's because a local declaration (with or without its semicolon, which is normally part of its syntax) is not an expression. If you wanted to do something like that with this proposal, you'd have to write
(int x = 12; M(x, x*x))
@paulomorgado commented on Wed Oct 21 2015
OK! Got it, @gafter!
My "See?" comment was regarding this:
@paulomorgado:
and this:
@ufcpp
But given your last example, sequence is starting to make sense to me.
@TyOverby commented on Wed Oct 21 2015
@gafter Since you invited the bikeshedding, I think "chained expressions" would be a nice name. It doesn't have the connotation of sounding like a list or a generator, and the "chain" imagery helps show that expressions can depend on results produced by previous ones.
@quinmars commented on Thu Nov 19 2015
FYI, gcc calls a similar C extension "statement expression".
@qrli commented on Fri Dec 18 2015
Seems it will introduce a little inconsistency with the for statement, which uses comma.
Ugly code:
Though people won't mix them in practice.
It will also be possible to write silly program like this, which does not look like C#:
@alrz commented on Fri Dec 18 2015
@qrli First example could be written like
And in the second example, last inner semicolon is a syntax error.
By the way, this is just a matter of taste, you should not use expression-bodied methods wherever you can, but you might do so if you prefer.
@leppie commented on Thu Mar 24 2016
Sub
@alrz commented on Mon Dec 05 2016
This conflicts with current scoping of is var and out var
Probably this should be decided now if this feature is planned for a future release.
The text was updated successfully, but these errors were encountered: