Proposal: Sequence Expressions #8932
Replies: 50 comments
-
It looks like the LDM recently voted down declaration expressions (again). Is this proposal meaningfully different to warrant separate consideration? |
Beta Was this translation helpful? Give feedback.
-
@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? |
Beta Was this translation helpful? Give feedback.
-
@gafter It's pretty terse but I assumed that was what this was referring to. |
Beta Was this translation helpful? Give feedback.
-
@MgSam I think that's talking about allowing code like: if (condition)
var foo = bar; |
Beta Was this translation helpful? Give feedback.
-
@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? |
Beta Was this translation helpful? Give feedback.
-
@MgSam That's not what embedded statement means. |
Beta Was this translation helpful? Give feedback.
-
Quite right. My mistake. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
That would conflict with existing block syntax since the expression is not at all dependent on an assignment. |
Beta Was this translation helpful? Give feedback.
-
@Richiban you could probably rewrite the grammar to make blocks expressions, but |
Beta Was this translation helpful? Give feedback.
-
@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";
}
} |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
What would be the benefit of |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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- |
Beta Was this translation helpful? Give feedback.
-
And for field initializers (e.g. in a record declaration)? Or the loop completion condition in a |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
Seems very unlikely to happen, ever. It is certainly much more complex than sequence expressions. |
Beta Was this translation helpful? Give feedback.
-
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). |
Beta Was this translation helpful? Give feedback.
-
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.
|
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
Do you mean an anonymous type initializer? But there is no |
Beta Was this translation helpful? Give feedback.
-
No. I mean normal initializer expressions. There used in a few places. Like: |
Beta Was this translation helpful? Give feedback.
-
But you still have |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
See #3086 for the version of this proposal with braces. |
Beta Was this translation helpful? Give feedback.
-
@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.
Beta Was this translation helpful? Give feedback.
All reactions