-
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: Expression blocks #3086
Comments
In terms of impl, this will be a shockingly easy mistake to make (i do it all the time myself). We shoudl def invest in catching this and giving a good message to let people know what the problem is and how to fix it. i.e. if we detect not enough expr args, oing in and seeing if replacing with a semicolon with a comma would fix things and pointing peoplt to that as the problem. |
Is this for ease of impl, or is there a really important reason this doesn't work at the language level? for example, i don't really see any issues with continuing (to a containing loop) midway through one of these block-exprs. |
I also don't see the reasons for any of the restrictions TBH, other than expression trees. |
The evaluation stack may not be empty at the int sum = 0;
foreach (int item in items)
{
sum = sum + { if (item < 3) continue; item };
} |
Riht... but why would i care (as a user)? From a semantics perpective, it just means: throw away everything done so far and go back to the for-loop. I can get that this could be complex in terms of impl. If so, that's fine as a reason. But in terms of hte language/semantics for the user, i dont' really see an issue. |
@CyrusNajmabadi as a user I find the example by @cston hard to grok. Yanking the whole conditional statement out of the expression block makes everything MUCH clearer. Do you have a counterexample where return, break or continue work better inside an expression block? |
In terms of impl, we should look at the work done in TS here. in TS |
Consider the following:
A block which executes two statements inside, with an empty statement following.
An expression-statement, whose expression is a block expression, with a statement, then the evaluation of 'b'. Would we allow a block to be the expression of an expr-statement? Seems a bit wonky and unhelpful to me (since the value of hte block expression would be thrown away). Should we only allow block expressions in the case where the value will be used? |
@cston To avoid the look-ahead issue, I would suggest an alternative change:
This means that we always parse I think this would solve the look-ahead issue for the compiler, but not so much for humans. I'd still favor |
yes. I'm very on board with a different (lightweight) sigil to indicate clearly that we have an expr block |
How about |
I wonder if the ASP.NET team would lean their preference to |
Isn't that a good reason *not* to use it, then, as it may cause parsing
issues in a Razor/Blazor page?
…On Tue, 7 Jan 2020 at 21:39, Joe4evr ***@***.***> wrote:
I wonder if the ASP.NET team would lean their preference to @{ since
that's already established for a statement block in Razor syntax. 🍝
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#3086?email_source=notifications&email_token=ADIEDQLRWL7SSRWNJ7IZWSDQ4TY73A5CNFSM4KD5XJAKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEIKL6VY#issuecomment-571785047>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/ADIEDQKNNDPOSBT2TKGFOMTQ4TY73ANCNFSM4KD5XJAA>
.
|
This is kinda neat but the syntax definitely bothers me as being too subtle of a difference for block vs expression. I think having |
I'm not bothered by the semicolon, but understand the potential confusion. Also, if I undestand correctly, it will not be possible to simply relax the syntax and let the compiler decide whether the block is statement / expression due to lambda type inferrence. Correct? |
I think this is really promising, and a good starting point. We've been circling around the possibility of being able to add statements inside expressions for many years. I like the direction of this proposal, because:
Within that, I think there are several design discussions for us to have:
At the end of the day, this is the kind of feature that, even when we've done the best we can on designing it, it just doesn't feel right and we end up not doing it. Putting statements inside expressions may just fundamentally be too clunky to be useful. |
I'd love it if this were possible without requiring a modified syntax. Sure, I understand that this would change the meaning of existing code, but most of the time that change would be that a value is harmlessly discarded. I am aware of at least one situation where this could affect overload resolution for lambda expressions, are there others? |
If I'm understanding the proposal correctly this would feel very weird when used with expression-bodied members. class A
{
int Foo() => 5; //Expression
int Foo2() => { ; 5 } //Expression block?
int Foo3() => { return 5; } //Not allowed
} |
@MgSam that's what Mads is pointing out with "Should a block expression be allowed as a statement expression? probably not!" |
If statement expressions are added, and "Control cannot leave the block other than after the trailing expression", there's increased incentive to make conditionals more user friendly, so that it's easier for the result of a block expression to depend on a test. I find deeply nested conditional expressions highly unreadable. This suggests that we should allow if-else expressions. This also cuts the other way. With sequence expressions it's much easier to turn an In scala and rust it's common for the entirety of a method to consist of a single expression consisting of multiple nested if-else expressions. I find this to be a really nice style. |
If I understand correctly the main motivation of this proposal is only switch statements #3038. Really I don't see another value benefits from this, much more desirable for me it is something like Consider slightly changed @MadsTorgersen example var length =
{
var (x, y) = (GetX(), GetY());
Math.Sqrt(x*x + y*y);
} much more clear and obvious for me var (x, y) = (GetX(), GetY());
var length = Math.Sqrt(x*x + y*y); or hide variables into functional scope double CalculateDistance(double x, double y) => Math.Sqrt(x*x + y*y);
var length = CalculateDistance(GetX(), GetY()); So, from this point double CalculateDistance()
{
var (x, y) = (GetX(), GetY());
return Math.Sqrt(x*x + y*y);
}
var length = CalculateDistance(); var length =
{
var (x, y) = (GetX(), GetY());
return Math.Sqrt(x*x + y*y); // it should contains explicit 'return'
} But I am not sure that this is really important and value feature... |
The expression block can take place in a deeply nested expression, where converting it to a set of statements would require significant refactoring. |
I think
Ternary operator and object initialization will benefit from this too. var grid = new Grid {
Children = {
({
var b = new Button { Text = "Click me" };
Grid.SetRow(b, 1);
b
})
}
}; |
@YairHalberstadt, can you provide an example? @ronnygunawan, seems looks more clear... Button CreateClickMeButton()
{
var b = new Button { Text = "Click me" };
Grid.SetRow(b, 1);
return b;
}
var grid = new Grid {
Children = {
CreateClickMeButton()
}
}; |
@0x000000EF When building deeply nested UIs using code it is often desirable to have the elements declared right where they are in the tree, not split off somewhere else. It mirrors the equivalent XAML/HTML/etc more closely and it's easier to reason about the structure of the UI.
I'm not sure what you mean by that. I think it's useful to be able to reason about the difference in behavior between...
...with something less subtle than just the absence of the semicolon, particularly if the proposal to implicitly type lamdas to |
@mikernet, it is not a big problem if we have something like static T With<T>(this T b, Action<T> with)
{
with(b);
return b;
}
var grid = new Grid {
Children = {
new Button { Text = "Click me" }.With(b => Grid.SetRow(b, 1))
}
}; |
The use of curly braces for expression blocks would break the current grammar in several cases. I propose the following syntax, which to my knowledge would extend conservatively the current grammar. Moreover, it seems to me somehow more natural.
Typing Rules
With my proposal would became: |
What cases are these? |
maybe it will not break the grammar, but sometimes it would look just odd.
why should not '{ i }' be parsed as an expression block with an empty statement list?
a semicolon would make a big difference in the inferred type. also this would look quite odd to me. |
@ltagliaro I don't see how that would break the grammar. It would simply be an error. |
This one looks suspicious: $$$"""{{{{C c = new(); c}}}}""" |
On Tue, Sep 24, 2024 at 12:14 PM ltagliaro ***@***.***> wrote:
The use of curly braces for expression blocks would break the current
grammar in several cases. I propose the following syntax, which to my
knowledge would extend conservatively the current grammar. Moreover, it
seems to me somehow more natural.
*Expression_block : '(' statement_list ';' expression ')';*
Examples:
var x = (var temp = f(); temp * temp + temp);
var x = (Console.WriteLine("Logging..."); var temp = f(); temp * temp + temp);
That's way too much lookahead.
It's too much lookahead for a whole class of parsers, LR parsers, the one
usually used for programming language parsing.
It's too much lookahead for a human. And too unobvious. It should be easy
to tell whether something is a code block or not. Having to spot a
semi-colon buried in code that could be many lines long is a horrible idea.
Message ID: ***@***.***>
… |
Isn't it the same for the original proposal?
to tell apart an expression block from a regular one it is necessary to look at the last chunck: if there is a semicolon, it's a regular block; if there is no semicolo, it's a block_expression. |
There are multiple things are already disallowed directly inside an interpolation hole: ternaries, for example, must be wrapped in parentheses. This would be no different. Errors are not the same as ambiguities; what your comment suggests to me and Cyrus is that you found some ambiguity, where there could be multiple different valid parses of an expression, or some expression that, while it can technically be parsed, would be extremely difficult to do so. |
@333fred |
@333fred using System.Collections.Generic;
var c = new C { X = { null } };
class C
{
public List<object?> X = new();
} Is it a direct assignment of the expression |
That's not necessarily an ambiguity; the existing meaning continues to be what it means. However, I do agree that is potentially confusing for a human reader. |
It's sort of the same ambiguity with indexer initializers: using System.Collections.Generic;
public class C {
public void M() {
_ = new List<int[]> { [1,2,3] }; // error; expected `[..] = expr`
_ = new List<int[]> { ([1,2,3]) }; // ok
}
} Block-expressions could be disambiguated the same way, using |
I created this which is closely related to this topic: #8471 But I think the syntax is more clean and universal by allowing the usage of of object.{/expressions/} universally. |
Right now, we can already kind of do that, but the syntax is just ugly and the performance might not be ideal in certain situations: int number = ((Func<int>)(() =>
{
int sum = 0;
for (int i = 1; i <= 100; i++)
{
sum += i;
}
return sum;
}))(); In the example above, we are just creating a lambda function and immediately invoking it. The performance debit might come when the lambda function needs to capture outside variables, creating closures. Also, the lambda function only produces a value, but doesn't consume a value from a previous expression. That one is already possible as well, but in a very hacky and ugly way: string words = "Hello world!"
.Replace("world", "universe")
is string s ? ((Func<string, string>)((s) =>
{
for (int i = 0; i < 10; i++)
{
s += "[" + i + "]";
}
return s;
}))(s) : ""; The challenge with the previous result is accessing the current value of the expression. I'm using the ternary logical operator with the So, what I'm trying to say with this reply is that I see expression blocks just as anonymous methods/lambdas that are immediately invoked once. Thus, looking at them as just methods/lambdas, there would be no need to have special keywords/syntax for returning values and things like that — just treat them as syntactic sugar for an anonymous local method. As for the conflict with object and collection initializers, in my proposal in another post I suggest always requiring an object initializer when you want to use expression blocks after the MyObject obj = new() { // Empty initializer } { // Expression block } In the shown example, you would need to include an empty initializer ( MyObject obj = (new MyObject()) { // Expression block } Please let me know what do you guys think of treating expression blocks as anonymous local methods. |
What syntax are you suggesting? That C# support IIFE, or that expression blocks are lambda method bodies? |
Kind of both. I look at expression blocks as lambda/method bodies that are immediately invoked. As I see it, an expression block like this: int number = { return 10; } Could be syntactic sugar for this: int AutoGeneratedInlineMethod() { return 10; }
int number = AutoGeneratedInlineMethod(); |
I think that runs into the same issues as the following already means something different: { return 10; } |
I'm not sure what you mean, and maybe I understood it incorrectly. An expression block would be allowed only where some value is expected — that is, as the right operand of an assignment (=), as a parameter to a method, property indexer, constructor, as an operand to some operator, etc. For example: int SomeMethod()
{
{ return 10; } // This is not allowed as an expression block, because the value is not passed to anything. This is the method's return statement, but inside brackets.
}
int MethodWithExpressionBlock()
{
{ return { return 10; }; } // This should be allowed, because the expression block returns 10 as a value to the method's return statement.
} |
That would be a drastic change between statement and expression form, especially since in most (all?) cases in C# expressions don't completely change their nature based on whether or not they are assigned to something. That's my opinion, anyway. My understanding is that at least some of the members of the language team do like the idea of "expression blocks" basically being local functions without the ceremony. |
I understand your point, and I agree with you. However, I'm not exactly suggesting that expressions change their behavior depending on when they are used. What I'm saying is that, other than method calls, using a value in a place where something is not expecting it is already invalid C# anyway. So, you could use expression blocks wherever you could write value literals or lambda functions, for instance. Take a look at the following example: int MyMethod()
{
// This is invalid C#.
"Some string";
// This lambda is also invalid C#. Remember that expression blocks could just be syntactic sugar for this (but immediately invoked).
() => { return 10; };
// This is valid C#, and is not an expression block.
{
Console.WriteLine("This is valid");
}
// Therefore, this is also not an expression block.
{ return 10; }
} |
What is the meaning of:
And how do I return from the method inside the expression? |
I have been propose var i = :{
if (DateTime.Today.DayOfWeek == DayOfWeek.Sunday)
return 5;
return 6;
} Even And 249 state. What we really want is anonymous immediate local function. We want local function that would be anonymous and just execute immediately |
@ayende |
Another way I think is good for implementing this, but without the confusion, would be to just allow a lambda to be called immediately after declaration, returning its value, like others have mentioned. Like this: int i = () => { return 10; }(); Because the lambda would be invoked, the lambda itself would never be used as a value, and thus it would never need to capture variables and would not need closures. This still has the limitation of not being possible to access the value of the previous expression. However, to solve this, a new operator could be introduced to access the current expression value, similar to what you can do with the string words = "Hello world!"
.Replace("world", "universe")
is string s ? ((Func<string, string>)((s) =>
{
for (int i = 0; i < 10; i++)
{
s += "[" + i + "]";
}
return s;
}))(s) : ""; In the previous example, I'm using a combination of the ternary logical operator and the string words = GetWords() s s.Replace("world", universe); The previous example, shows a roughly thought way for assigning the value of an expression to a variable created inline. This is similar to string s = GetWords();
string words = s.Replace("world", universe); With a way to access the value of the previous expression, like in the previous example, and being able to immediately invoke a lambda, we can turn this: string words = "Hello world!"
.Replace("world", "universe")
is string s ? ((Func<string, string>)((s) =>
{
for (int i = 0; i < 10; i++)
{
s += "[" + i + "]";
}
return s;
}))(s) : ""; Into this: string words = "Hello world!"
.Replace("world", "universe")
s () =>
{
for (int i = 0; i < 10; i++)
{
s += "[" + i + "]";
}
return s;
}(); Because the lambda is immediately executed, 's' would not be captured by the lambda, ensured by the compiler. In this case, the lambda is an With this approach, there would be no expression blocks whatsoever. There would be only immediately invoked lambdas and an "expression identifier" operator. Expression blocks would be just a side effect. This approach would be an alternative way for avoiding the confusion with the expectations people have with how to return from an expression block and things like that. This way, people would be working with the familiarity of lambdas. Also, the "expression identifier" operator would be very useful for other things as well. |
See: #4748 |
Thinking expression blocks of a local method or a lambda seems a roundabout way to me. Using local method or lamda is just a workaround of current C# not having expression blocks. With expression blocks, such redundant method calls would be unnecessary. From my point of view, expression block is a syntax that allows statements to be executed during expression evaluation. No method call is involved. (Expression block syntax could exist even in a hypothetical language that doesn't support any functions.) Re-defining |
For me personally, the expressions blocks were a way to keep DRY and also work around not having the cascade operator (granted I'm open to that being an alternative as well.) For reference see the cascade operator in flutter: Example:
But I really like the "{}" block notation. It's technically more dry by not having to repeat the ".." over and over, granted not as much overhead DRY reduction as not having to repeat the variable name, but still a net win! The cascade operator might be more readable in some cases, but overall I think the curly brackets for dot notation is the best. |
That looks like a separate concern from expression blocks to me. I think a forward pipe operator could handle both of those cases. #96 and #74. Just pipe the result into the expression. var paint = Paint() |>
${
@.color = Colors.black
@.strokeCap = StrokeCap.round
@.strokeWidth = 5.0;
yield @;
} Although, that could get complicated with nested pipes... Maybe it's not such a great idea after all. Maybe it would be ok if the pipe operator would let you name the piped value |
I don't think expression blocks as proposed/discussed here are related to cascade operators, initializers or the like. You're not starting from a value and composing over that value. At most you're working with the identifiers already in scope. Any statements within the block itself would be self-contained, like a statement block or a local function with no arguments. |
Proposal
Allow a block of statements with a trailing expression as an expression.
Syntax
Examples:
Execution
An expression block is executed by transferring control to the first statement.
When and if control reaches the end of a statement, control is transferred to the next statement.
When and if control reaches the end of the last statement, the trailing expression is evaluated and the result left on the evaluation stack.
The evaluation stack may not be empty at the beginning of the expression block so control cannot enter the block other than at the first statement.
Control cannot leave the block other than after the trailing expression unless an exception is thrown executing the statements or the expression.
Restrictions
return
,yield break
,yield return
are not allowed in the expression block statements.break
andcontinue
may be used only in nested loops orswitch
statements.goto
may be used to jump to other statements within the expression block but not to statements outside the block.out
variable declarations in the statements or expression are scoped to the expression block.using expr;
may be used in the statements. The implicittry
/finally
surrounds the remaining statements and the trailing expression soDispose()
is invoked after evaluating the trailing expression.Expression trees cannot contain block expressions.
See also
Proposal: Sequence Expressions #377
LDM 2020-01-22
https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-26.md#discriminated-unions
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-08-28.md#block-bodied-switch-expression-arms
The text was updated successfully, but these errors were encountered: