csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

Proposal: Sequence Expressions

Open gafter opened this issue 7 years ago • 42 comments

@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.

gafter avatar Mar 28 '17 19:03 gafter

Dupe of ~~#9~~#72? 😀

orthoxerox avatar Mar 28 '17 19:03 orthoxerox

It looks like the LDM recently voted down declaration expressions (again). Is this proposal meaningfully different to warrant separate consideration?

MgSam avatar Mar 28 '17 20:03 MgSam

@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 avatar Mar 29 '17 00:03 gafter

@gafter sorry, #72

orthoxerox avatar Mar 29 '17 05:03 orthoxerox

@gafter It's pretty terse but I assumed that was what this was referring to.

MgSam avatar Mar 29 '17 12:03 MgSam

@MgSam I think that's talking about allowing code like:

if (condition)
    var foo = bar;

svick avatar Mar 29 '17 13:03 svick

@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 avatar Mar 29 '17 13:03 MgSam

@MgSam That's not what embedded statement means.

svick avatar Mar 29 '17 13:03 svick

Quite right. My mistake.

MgSam avatar Mar 29 '17 13:03 MgSam

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 avatar Apr 05 '17 14:04 Richiban

@Richiban

That would conflict with existing block syntax since the expression is not at all dependent on an assignment.

HaloFour avatar Apr 05 '17 14:04 HaloFour

@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 avatar Apr 05 '17 18:04 orthoxerox

@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";
	}
}

Richiban avatar Apr 06 '17 09:04 Richiban

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.

alrz avatar Nov 30 '17 18:11 alrz

What would be the benefit of if (var x = e; x) over var x = e; if (x)?

quinmars avatar Dec 02 '17 11:12 quinmars

@quinmars https://github.com/dotnet/csharplang/issues/595

alrz avatar Dec 02 '17 12:12 alrz

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.

quinmars avatar Dec 02 '17 12:12 quinmars

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 avatar Dec 02 '17 12:12 alrz

@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.

qrli avatar Dec 02 '17 14:12 qrli

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.

alrz avatar Dec 02 '17 14:12 alrz

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?

gafter avatar Sep 11 '19 21:09 gafter

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?

orthoxerox avatar Sep 11 '19 21:09 orthoxerox

Are there any leaky variables beyond out vars that might complicate the decision?

Patterns?

CyrusNajmabadi avatar Sep 11 '19 22:09 CyrusNajmabadi

Oh, right. But I think they have the same solution, since they are leaky for the same purpose.

orthoxerox avatar Sep 11 '19 22:09 orthoxerox

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.

alrz avatar Sep 11 '19 23:09 alrz

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.

munael avatar Sep 12 '19 15:09 munael

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.

orthoxerox avatar Sep 12 '19 17:09 orthoxerox

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);

Thaina avatar Dec 01 '19 06:12 Thaina

this doesn't seem to be an improvement at all over how this would be written today.

CyrusNajmabadi avatar Dec 01 '19 17:12 CyrusNajmabadi

@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

Thaina avatar Dec 01 '19 18:12 Thaina