csharplang
csharplang copied to clipboard
Proposal: Sequence Expressions
@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
parenthesized-expression:
(
sequence-statementsopt expression)
sequence-statements: sequence-statement sequence-statements sequence-statement sequence-statement: expression-statement declaration-statement
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:
- What is the scope of variables declared in these statements, given our existing scoping rules?
- What kinds of statements are permitted? E.g. we might forbid control transfers out of the expression.
- How does this interact with a tuple expression?
- How much value is there in this feature now that we have local functions?
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 :+1:
@orthoxerox commented on Tue Oct 20 2015
Why not
parenthesized-expression:
{
sequence-statementsopt expression-statement}
sequence-statements: sequence-statement sequence-statements sequence-statement sequence-statement: expression-statement declaration-statement
?
@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, because 1+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:
Aren't you afraid it might be confused with "as opposed to asynchronous expressions"?
and this: @ufcpp
I receive the impression from the name "sequence expression" that its result type is an array of each expression.
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:
for (int i = 0, j = 1; (int k = i + j; M(k, k)); i++, j *= 2)
{
}
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#:
public double Foo() => (
int a = GetSomething();
Log(a);
DoSomething(a);
);
@alrz commented on Fri Dec 18 2015
@qrli First example could be written like
for (int i = 0, j = 1; i + j case var k : M(k, k); i++, j *= 2)
And in the second example, last inner semicolon is a syntax error.
public double Foo() => (int a = GetSomething(); Log(a); DoSomething(a));
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
The scope of any variable declared in a parenthesized-expression is the body of that parenthesized-expression.
This conflicts with current scoping of is var and out var
var x = (e is T t ? t : foo);
// t is in scope!
Probably this should be decided now if this feature is planned for a future release.
Dupe of ~~#9~~#72? 😀
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?
@gafter sorry, #72
@gafter It's pretty terse but I assumed that was what this was 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 return
for the syntax? Currently lambdas have two different syntaxes depending on whether they're statement-bodied or expression-bodied.
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:
arguments_list '=>' expression | arguments_list '=>' statement_block
only
arguments_list '=>' expression
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 =>
symbol:
public string DoStuff() =>
{
Log("Doing stuff");
return "some result";
}
I think this results in a more consistent syntax overall.
@Richiban
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 return
is out of the question, since it always returns from the method, not the innermost block.
@orthoxerox Ahh, you mean that the meaning of return
would be hugely different in the two following cases:
string MyMethod()
{
var name = {
Log("Getting name");
return "John";
}
return "Dave";
}
and
string MyMethod()
{
{
Log("Getting name");
return "John";
}
}
sequence-statement:
expression-statement
declaration-statement
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 break expr
, break;
and return;
statements are permitted and would work the same way that they do today.
PS: we could instead of "sequence expressions" we just introduce if
, switch
initializes (a la C++)
if (var x = e; x)
switch (var x = e; x)
However, https://github.com/dotnet/csharplang/issues/1090 eliminates double parens even if we had sequence expressions independently.
What would be the benefit of if (var x = e; x)
over var x = e; if (x)
?
@quinmars https://github.com/dotnet/csharplang/issues/595
Yes, for while loops this might make sense, but if
condintions have leaking scope. So if (var x = e; x)
and var x = e; if (x)
would be the same. I don't remeber if switch
is leaky or not, but I guess it is.
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(), ...
}
return
in a local function doesn't return from the enclosing method, but the local function itself.
@alrz I like your idea. But for the return
, I think it is confusing. The match syntax is like lambda, but the meaning of return
would be very different. That's subtle and can be easily overlooked.
I think it should forbid return
at all, since it is inside an expression.
it should forbid return at all, since it is inside an expression.
See https://github.com/dotnet/csharplang/issues/867 which would allow return
as an expression.
I have championed this issue and added the following notes:
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:
- What is the scope of variables declared in these statements, given our existing scoping rules?
- What kinds of statements are permitted? E.g. we might forbid control transfers out of the expression.
- How does this interact with a tuple expression?
I'd say a sequence expression should have its own scope. Variables declared inside it do not leak outside.
Are there any leaky variables beyond out vars that might complicate the decision?
Are there any leaky variables beyond out vars that might complicate the decision?
Patterns?
Oh, right. But I think they have the same solution, since they are leaky for the same purpose.
so this is as close as we can get to "block expressions"?
What kinds of statements are permitted? E.g. we might forbid control transfers out of the expression.
I really wanted to do var x = e switch { …, _ => { continue; } };
but I guess it wouldn't be possible with sequence expressions.
What is the scope of variables declared in these statements, given our existing scoping rules?
I think making the scope to not leak out of itself at all would be too restrictive, I'd suggest we give it the "enclosing scope" which has been originally proposed for patterns and out var
if ((var x = M(); f(x))) {
// x still in scope
}
// but not here
var a = (var x = M(); f(x)); // ok
var b = M(arg, (var y = M(), f(y)), y); // ok
// neither x nor y would not be in scope here
Further, if we include empty-statement among valid statements, it'd enable to restrict pattern and out var scopes as well.
if ((;; o is string s)) { }
// s wouldn't be in scope here because it now follows seq-expression scoping roles!
Obviously skipping double parens would definitely be something to consider.
What is the scope of variables declared in these statements, given our existing scoping rules?
Same as for normal variables in current sequence statements. Nothing leaks. Its primary use case would be to get the return result somewhere without polluting the enclosing scope with temporary variables. If they're supposed to leak, why not just put them in the enclosing scope?
Borrowing an example (@alrz):
var a = (var x = M(); f(x)); // ok
var b = M(arg, (var y = M(), f(y)), y); // not ok, leaks
var c = (
var y = M();
M(arg, f(y), y)
); // ok instead of b
What kinds of statements are permitted? E.g. we might forbid control transfers out of the expression.
Honestly, can't think of any sensible restrictions. As expressions, they have to evaluate to a value in the end. But there's no reason to prohibit them from making the enclosing function return or throw. Although returning might be too hidden of a path to allow, throwing should probably not be restricted. What other control transfers are there?
And, since it's a normal scope, continue
and co should work as normal.
How does this interact with a tuple expression?
Probably quite a bit of look-ahead. Tuples can have semicolons already (in lambdas) so I'm not sure.
In my old prototype I parsed them in ParseCastOrParenExpressionOrLambdaOrTuple
before trying a paren expression or a tuple. But back then only declarations were allowed as statements inside a sequence expression, so there was a simple lookahead check for them.
Additionally
I think Sequence Expressions will be great if we could use it in conjunction with single line try/catch
block
var user = try (DoSomething();Loging();await GetUserFromDB(userID)) catch(Expression ex) => (LogException(ex);null);
this doesn't seem to be an improvement at all over how this would be written today.
@CyrusNajmabadi Nowaday you can't assign variable out of try
block. There was already some proposal on try expression
I just add that we could also write try expression to assign variable and also write some expression that may throw exception and affect the assignment in the same try block