-
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))
}
}; |
Special case lambdas? var a = () => { // Prints 11 |
As someone who had to switch from F# to C#, I think this feature should be high priority. "Everything is an expression" is probably the most essential trait of functional programming. It seems like a minor distinction at first, but when you get used to conditional blocks having a return value, you start structuring all your code in terms of what you want the final value to be rather than the process. Something like: var userAccess = {
if(credentials ≠ null)
if(check(credentials))
AuthenticationOk;
else{
Logger.Warning("Invalid access attempt");
InvalidCredentials;
}
else Anonymous;
} Which keeps all the logic neatly contained. And with C# having already implemented most of the other parts of FP (like immutable objects), it hurts to see it so close to allowing functional-style code but still missing the ability to do this. |
I still think it is a bad idea if there wouldn't be a keyword for "this is what comes out of the expression block" and it would be just by convention that the last part of a expression block has to be a expression that is then returned. I still have big sympathies for an expression with the return ${ f(); out e; }; |
@dersia I fully agree. I'd love it if Examples: var userAccess = ${
if(credentials ≠ null)
if(check(credentials))
return AuthenticationOk;
else{
Logger.Warning("Invalid access attempt");
return InvalidCredentials;
}
else return Anonymous;
};
var r = ${
Span<int> sp = stackalloc int[1];
ref int r0 = ref sp[0];
return r0 + x;
}; just translates to var userAccess = (() => {
if(credentials ≠ null)
if(check(credentials))
return AuthenticationOk;
else{
Logger.Warning("Invalid access attempt");
return InvalidCredentials;
}
else return Anonymous;
})();
var r = (() => {
Span<int> sp = stackalloc int[1];
ref int r0 = ref sp[0];
return r0 + x;
})(); |
@mrwensveen I would really not like this. Immediately invoked lambda means that you will be allocating. Consider:
If it is lowered to:
Look at the second implementation:
Those are two allocations to pay for this feature (one for the state, one for the delegate). It should be effectively just lexical scope added, nothing more. |
@mrwensveen I do a lot of code reviews and from that point of view I see problems with using There is also another nice trick that we get with public int Total { get; set; }
public void FooBar(int number1, int? number2)
{
int foo = ${
if (number2 is null)
{
return;
}
int result = number1 + number2;
out result;
}
Total = foo;
} this would give me an escape path, if I don't want to finish the block expression by just using |
I don't really see the point of the At least for |
@GeirGrusom I'm having trouble trying to visualize what you mean. How would you use a block expression in a try block? Why would you need to use block expressions in a switch statement? And why does the presence or absence of |
I think the desire is that all statements eventually can become expressions in C#, which is something that the language team has expressed not being interested in pursuing. Most of the reason why a method of differentiating expression blocks comes from the fact that it's not possible to implement them as such without breaking changes. |
Well, you'd end up with two syntaxes for the same thing. One example here: var catfact = try // Should there be a sigil here?....
{
if(cfg is CatFactFromDb)
out Db.GetCatFact();
else
out Net.GetCatFact();
}
catch(OperationCancelledException)
{
out Db.GetCatFact();
} Here there is no sigil, and why would there be. However if you have an expression block, then it for seemingly no good reason you need a sigil: var catfact = ${
if(cfg is CatFactFromDb)
out Db.GetCatFact();
else
out Net.GetCatFact();
}; In my opinion the sigil does absolutely nothing. It's nothing more than pointless ceremony for the sake of pointless ceremony. It does nothing for the compiler that the compiler can't figure out without it, it does not actually add anything of importance to the reader, since it's very clear from the usage that this is going to be an expression block, and if there is any confusion the compiler will produce warnings and errors. And if the compiler team wants to make more statements into expressions, then it's just going to add confusion, and in my opinion, a wart in the language. Edit: What I want to ask is, is I don't think the sigil will noticibly improve readibility, but adds a syntactic notation for something where you'll just end up annoyed that you have to go back to the beginning and add a ceremonial symbol just because that's what the syntax demands. Edit2: Removed a bit that might have unintentionally seemed a bit hostile. |
Maybe that's a word salad, but in my opinion it's just natural that syntax are expressions, and the point of this feature is more that the language now allows blocks to be expressions (because that would not have compiled in earlier versions of the language), and add a statement to return a value in those blocks. If there's a sigil then it's a separate thing, and it adds syntax to the language that I don't think actually adds any value. It's just "remember to put this symbol here if you want to use this existing language construct in that context!" |
BlocksMy thinking starts with "how does a code block work right now"?: public int SomeMethod()
{
// Do some stuff
var c = 0;
// For some reason, we don't want context in the block to leak out.
{
var a = 5;
var b = 4;
c = a + b;
}
// Some more stuff
return c;
} From this starting point, what would feel intuitive to me is using a public int SomeMethod()
{
// Do some stuff
var c =
{
var a = 5;
var b = 4;
return a + b;
}
// Some more stuff
return c;
} Syntactically, this would be close to what has been mentioned by others (an immediately invoked function): public int SomeMethod()
{
// Do some stuff
var c = new Func<int>(() =>
{
var a = 5;
var b = 4;
return a + b;
})();
// Some more stuff
return c;
} This wouldn't have to Terseness / If statementsWith the following example, I think I do prefer not having public int SomeMethod()
{
return { var a = 5; var b = 4; a + b; }
}
public int SomeMethod(int val)
{
if ({ var val2 = val + 2; val2 + 2 } == 10)
{
// do stuff
}
} Vs. public int SomeMethod()
{
return { var a = 5; var b = 4; return a + b; }
}
public int SomeMethod(int val)
{
if ({ var val2 = val + 2; return val2 + 2 } == 10)
{
// do stuff
}
} I think I'm at a tie here - sometimes I like having the |
Expanding on code blocks in this manner has been explored before. The problem is that code blocks are already legal syntax, making them into expressions would fundamentally change a lot of existing code in C#. For example, a block in a lambda body indicates that it is an The other problem is that |
Return would definitely change the behavior of existing code, which is why
The compiler can infer that expressions are code blocks in three ways: they're used as a value, they contain one or more statements and they contain at least one out statement, so array assignments aren't ambiguous. I don't think you can write existing code that this would change the meaning of without some mad hatter macro stuff. Lambdas would still work as they do. The compiler can issue a warning/error for expression blocks that are not used as a value, and compilation error for using a non-expression block as a value as it does today. I think this could work without sigils or completely different syntax withing a block. |
Is this really all of it? For example, you can infer that if we don't end the block expression with a return value (i.e. without the last value without a comma in the end) then this block expression is of a void type, and it is just natural that a void block expression would fallback into being an action. And it is to question if all blocks need to be expressions, should the scope block of a method be an expression and something like this be allowed in the current C#? int X()
{
10
} If it would not, then it would not be such of a problem to have only blocks not tied to any other scope with an already definite meaning to not to be expressions. Kotlin do something "in these lines" as well and their block expressions only apply when you use it with an if or switch expression (or similars), and the meaning of '{}' as functions is different when the '{}' are being used as scope delimiters in the language (with a Rust like approach this would be a problem, but not with something like Kotlin do). |
Yesterday, I added a proposal that is very close to this one. Shame on me for not having found that this one already existed, and even with an identical name. That discussion is fairly closed, due to it being a duplicate. However, I wrote some details on my take on expression blocks, and maybe those can add something useful to this ongoing discussion. For those interested, you can find it here. I must admit that I don't consider my proposal to be ideal, but maybe it can trigger some ideas. And, I also would like to add that, independently of the syntax you choose to implement such functionality, I would really love to have statements in expressions, or at least a way to not break an expression chain (for example, for the fluent pattern). I know you're considering ways to make that happen for quite a few years, and I have genuine hope that it could come soon (in the foreseeable future). So, I just want to let you know, that you have my +1 for this. |
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. |
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: