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

Proposal: Sequence Expressions #377

Open
gafter opened this issue Mar 28, 2017 · 50 comments
Open

Proposal: Sequence Expressions #377

gafter opened this issue Mar 28, 2017 · 50 comments

Comments

@gafter
Copy link
Member

gafter commented Mar 28, 2017

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


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

@orthoxerox
Copy link

orthoxerox commented Mar 28, 2017

Dupe of #9#72? 😀

@MgSam
Copy link

MgSam commented Mar 28, 2017

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

@gafter
Copy link
Member Author

gafter commented Mar 29, 2017

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

@orthoxerox
Copy link

@gafter sorry, #72

@MgSam
Copy link

MgSam commented Mar 29, 2017

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

@svick
Copy link
Contributor

svick commented Mar 29, 2017

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

if (condition)
    var foo = bar;

@MgSam
Copy link

MgSam commented Mar 29, 2017

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

@svick
Copy link
Contributor

svick commented Mar 29, 2017

@MgSam That's not what embedded statement means.

@MgSam
Copy link

MgSam commented Mar 29, 2017

Quite right. My mistake.

@Richiban
Copy link

Richiban commented Apr 5, 2017

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.

@HaloFour
Copy link
Contributor

HaloFour commented Apr 5, 2017

@Richiban

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

@orthoxerox
Copy link

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

@Richiban
Copy link

Richiban commented Apr 6, 2017

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

@alrz
Copy link
Member

alrz commented Nov 30, 2017

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, #1090 eliminates double parens even if we had sequence expressions independently.

@quinmars
Copy link

quinmars commented Dec 2, 2017

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

@alrz
Copy link
Member

alrz commented Dec 2, 2017

@quinmars #595

@quinmars
Copy link

quinmars commented Dec 2, 2017

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.

@alrz
Copy link
Member

alrz commented Dec 2, 2017

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.

@qrli
Copy link

qrli commented Dec 2, 2017

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

@alrz
Copy link
Member

alrz commented Dec 2, 2017

it should forbid return at all, since it is inside an expression.

See #867 which would allow return as an expression.

@gafter
Copy link
Member Author

gafter commented Jan 2, 2020

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

@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)
    {
    }
}

@binki
Copy link

binki commented Jan 2, 2020

@gafter @CyrusNajmabadi

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.

@CyrusNajmabadi
Copy link
Member

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.

@HaloFour
Copy link
Contributor

HaloFour commented Jan 2, 2020

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-this members and coming up with a syntax for invoking the base constructor mid-constructor feels like the better approach for that specific use case. Cramming sequence expressions into base arguments is very difficult to read.

@gafter
Copy link
Member Author

gafter commented Jan 2, 2020

And for field initializers (e.g. in a record declaration)? Or the loop completion condition in a for loop?

@munael
Copy link

munael commented Jan 3, 2020

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 blah escape scopes in a weird way (like from one argument's expression to another's) because it then works like normal scopes (because in (1) it's outside both arguments' expressions).


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:

  1. If field a exposes some name x used in the init of field b but field a is specified in the literal, do you still run a default init expression?
  2. You can hide any loops in functions called in the expression.


For the for loop case... it's okay.

@gafter
Copy link
Member Author

gafter commented Jan 3, 2020

For the base class constructor case, there's #2335 .

Seems very unlikely to happen, ever. It is certainly much more complex than sequence expressions.

@munael
Copy link

munael commented Jan 4, 2020

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

@a-downing
Copy link

a-downing commented Nov 20, 2022

Why couldn't this syntax be used?

int x = {
    string str = "123";
    Int32.TryParse(str, out var num) ? num : 0
};

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.

int x = {
    string str = "123";
    if(Int32.TryParse(str, out var num)) { num } else { 0 }
};

@CyrusNajmabadi
Copy link
Member

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.

@a-downing
Copy link

Do you mean an anonymous type initializer? But there is no new before the {, so if you see { without new where an expression is expected, you automatically know what it is.

@CyrusNajmabadi
Copy link
Member

No. I mean normal initializer expressions. There used in a few places. Like: int[] x = { ... } or new Foo { X = { ... } }. Or new X { { ... } }. Three are all legal syntaxes today that use { to start an expression.

@a-downing
Copy link

But you still have new before { in some cases and , separating things instead of ; in all cases.

@a-downing
Copy link

a-downing commented Nov 20, 2022

I guess there is this int[] arr = {0}; though.

And object?[]? o = { null }; would need to be disambiguated with object?[]? o = null;

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 object?[]? o = { ; null };

@CyrusNajmabadi
Copy link
Member

But you still have new before { in some cases

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.

@333fred
Copy link
Member

333fred commented Nov 20, 2022

See #3086 for the version of this proposal with braces.

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