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: Expression blocks #3086

Open
cston opened this issue Jan 7, 2020 · 304 comments
Open

Proposal: Expression blocks #3086

cston opened this issue Jan 7, 2020 · 304 comments
Assignees
Milestone

Comments

@cston
Copy link
Member

cston commented Jan 7, 2020

Proposal

Allow a block of statements with a trailing expression as an expression.

Syntax

expression
    : non_assignment_expression
    | assignment
    ;

non_assignment_expression
    : conditional_expression
    | lambda_expression
    | query_expression
    | block_expression
    ;

block_expression
    : '{' statement+ expression '}'
    ;

Examples:

x = { ; 1 };  // expression block
x = { {} 2 }; // expression block

y = new MyCollection[]
  {
      { F(), 3 }, // collection initializer
      { F(); 4 }, // expression block
  };

f = () => { F(); G(); }; // block body
f = () => { F(); G() };  // expression body

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 and continue may be used only in nested loops or switch 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 implicit try / finally surrounds the remaining statements and the trailing expression so Dispose() 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

@CyrusNajmabadi
Copy link
Member

{ F(); 4 }, // expression block

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.

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Jan 7, 2020

Control cannot leave the block other than after the trailing expression unless an exception is thrown executing the statements or the expression.

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.

@YairHalberstadt
Copy link
Contributor

I also don't see the reasons for any of the restrictions TBH, other than expression trees.

@cston
Copy link
Member Author

cston commented Jan 7, 2020

Control cannot leave the block other than after the trailing expression unless an exception is thrown executing the statements or the expression.

Is this for ease of impl, or is there a really important reason this doesn't work at the language level.

The evaluation stack may not be empty at the continue.

int sum = 0;
foreach (int item in items)
{
    sum = sum + { if (item < 3) continue; item };
}

@CyrusNajmabadi
Copy link
Member

The evaluation stack may not be empty at the continue.

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.

@orthoxerox
Copy link

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

@CyrusNajmabadi
Copy link
Member

In terms of impl, we should look at the work done in TS here. in TS { can start a block, or it can start an object-expr. Because of this, it's really easy to end up with bad parsing as users are in the middle of typing. It important from an impl perspective to do the appropriate lookahead to understand if something should really be thought of as an expression versus a block.

@CyrusNajmabadi
Copy link
Member

Consider the following:

{ a; b; } ;

A block which executes two statements inside, with an empty statement following.

{ a; b };

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?

@gafter gafter added this to the 9.0 candidate milestone Jan 7, 2020
@jcouv
Copy link
Member

jcouv commented Jan 7, 2020

@cston To avoid the look-ahead issue, I would suggest an alternative change:

block
  : '{' statement* expr '}'
  ;

This means that we always parse { ... as a block, even if it has a trailing expression. Then we can disallow in semantic layer.

I think this would solve the look-ahead issue for the compiler, but not so much for humans. I'd still favor @{, ${ or ({ to indicate this is an expression-block.

@CyrusNajmabadi
Copy link
Member

${

yes. I'm very on board with a different (lightweight) sigil to indicate clearly that we have an expr block

@HaloFour
Copy link
Contributor

HaloFour commented Jan 7, 2020

How about ={ 😁

@Joe4evr
Copy link
Contributor

Joe4evr commented Jan 7, 2020

I wonder if the ASP.NET team would lean their preference to @{ since that's already established for a statement block in Razor syntax. 🍝

@spydacarnage
Copy link

spydacarnage commented Jan 7, 2020 via email

@mikernet
Copy link

mikernet commented Jan 7, 2020

This is kinda neat but the syntax definitely bothers me as being too subtle of a difference for block vs expression. I think having $ as a prefix is more sensible and easier to recognize when reading.

@Trayani
Copy link

Trayani commented Jan 7, 2020

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?

@MadsTorgersen
Copy link
Contributor

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:

  • the {...} is recognizable from statement blocks. I know that curly braces are already somewhat overloaded, and there will be ambiguous contexts, but from a cognitive perspective I think it doesn't make the situation significantly worse, and is preferable to adding some new syntax for statement grouping.
  • It provides natural and easy-to-understand scoping for any variables declared inside, including those declared in the trailing expression (e.g. through out variables).

Within that, I think there are several design discussions for us to have:

  • Should the result be produced by a single expression at the end (as proposed here), or via a result-producing statement (e.g. break expr; has been proposed in Proposal: Block-bodied switch expression arms #3037 and Proposal: Enhanced switch statements #3038)? In the latter case it would be syntactically equivalent to a block statement, and just have different semantic rules (just as the difference between a block used for a void-returning vs a result-returning method). The former may work best for shorter blocks, the latter for bigger ones. Which should we favor?

  • Is the proposed precedence right? This disallows any operators from being applied directly to the statement expression. That's probably good, but needs deliberation. It limits the granularity at which an expression can easily be replaced with a block (though of course you can always parenthesize it, like every other low-precedence expression).

  • Should a block expression be allowed as a statement expression? probably not!

  • The proposal requires there to be at least one statement. That's kind of ok if the statement block is used for prepending statements to your expression! But once it's in the language I can imagine wanting to use it just to scope variables declared in a single contained expression.

  • I don't like the proposals for prepending a character so that you can "tell the difference", but that's another discussion to have. I don't think anyone other than the compiler team wants to "tell the difference". 😁

  • There's a potential "slippery slope" argument to allow other statement forms as expressions somehow. I don't think that's very convincing, since such statements should just be put inside of a block expression! But I can see that coming up.

  • We should make sure we gather the important scenarios. I've heard two really convincing ones:

    • as the branches of switch expressions (and switch statements if we do Proposal: Enhanced switch statements #3038). Switch expressions are themselves so complex that reorganizing the code to get a statement in becomes intrusive.
    • as "let-expressions" where a temporary local variable (or function) is created just for the benefit of one expression.

    Proposal: Block-bodied switch expression arms #3037 has examples of the former. An example of the latter might be:

    var length = { var x = expr1; var y = expr2; Math.Sqrt(x*x + y*y); }

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.

@HaloFour
Copy link
Contributor

HaloFour commented Jan 8, 2020

Allow a block of statements with a trailing expression as an expression.

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?

@MgSam
Copy link

MgSam commented Jan 8, 2020

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
}

@333fred
Copy link
Member

333fred commented Jan 8, 2020

@MgSam that's what Mads is pointing out with "Should a block expression be allowed as a statement expression? probably not!"

@YairHalberstadt
Copy link
Contributor

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 if-else with multiple statements into an expression. All you have to do is remove the final semicolon

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.

@0x000000EF
Copy link

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 with operator.

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 Expression blocks looks for me like a local function body without signature and parameters called immediately

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

@YairHalberstadt
Copy link
Contributor

YairHalberstadt commented Jan 8, 2020

@0x000000EF

The expression block can take place in a deeply nested expression, where converting it to a set of statements would require significant refactoring.

@ronnygunawan
Copy link

ronnygunawan commented Jan 8, 2020

I think this would solve the look-ahead issue for the compiler, but not so much for humans. I'd still favor @{, ${ or ({ to indicate this is an expression-block.

I think { is good enough. We can always parenthesize it as ({ when needed.

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 with operator.

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

@0x000000EF
Copy link

@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()
    }
};

@mikernet
Copy link

mikernet commented Jan 8, 2020

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

@MadsTorgersen

I don't think anyone other than the compiler team wants to "tell the difference"

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

f = () => { F(); G(); }; // block body
f = () => { F(); G() };  // expression body

...with something less subtle than just the absence of the semicolon, particularly if the proposal to implicitly type lamdas to Action/Func in the absence of other indicators gains traction. I guess the stylistic nature of the second example just feels a bit odd to me in the context of C# but maybe with time I'd get over that. A keyword before the trailing expression would solve that minor gripe as well but I'm not overly invested either way, just a suggestion to consider.

@0x000000EF
Copy link

@mikernet, it is not a big problem if we have something like with operator

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

@ltagliaro
Copy link

ltagliaro commented Sep 24, 2024

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

Typing Rules
In this syntax, the type of the expression block will be determined by the type of the last expression within the block.
Scoping Rules
An expression block introduces a scope. The scope of variables declared within the expression block must be limited to the block itself, similar to how variables in a traditional block {} are scoped.
Evaluation Rules
The final value of the expression block should be the result of the last statement inside the parentheses, allowing it to act like a traditional expression.
Consistency with C and C++ Semantics
The proposed use of the semicolon within an expression block in C# is consistent with the semantics of the semicolon operator in C and C++. In both languages, semicolons are used to separate multiple statements in an expression block (or compound statement), with the value of the entire block being the value of the final expression.
Consistency with Functional Languages
This proposal also aligns with the patterns found in functional languages, such as Haskell. In Haskell, the let ... in construct is used to declare variables in a local scope and then evaluate a final expression based on those bindings.
For example:

let a = 2
    b = 3
in a * b + a

With my proposal would became:
var x = (var a = 2; var b = 3; a * b + a);
Conclusion
The proposed syntax integrates well with conditional expressions and switch expressions, making it a natural extension of the language. Further improvements could include:
Return Statements in Expression Blocks:
Allowing return statements within expression blocks would enable a function to exit early. This return would not define the value of the expression block but would interrupt its evaluation and return from the enclosing function. This behavior would be similar to how the throw statement currently works in conditional, coalesce, and switch expressions.
Try-Catch Expression:
Introducing a try-catch expression, which would resemble the structure of a switch expression, would allow us to handle exceptions within an expression context. This addition would enable nearly any piece of code to be written as an expression, resulting in more concise and expressive code in certain situations.

@CyrusNajmabadi
Copy link
Member

The use of curly braces for expression blocks would break the current grammar in several cases

What cases are these?

@ltagliaro
Copy link

ltagliaro commented Sep 24, 2024

maybe it will not break the grammar, but sometimes it would look just odd.

int i = 42;
int[] a = { i };

why should not '{ i }' be parsed as an expression block with an empty statement list?
ok, because in the rule '{' statement+ expression '}' there is a +, not a *.
but it looks quite odd to me!

    internal static class Class1
    {
        private static object a() => throw new NotImplementedException();
        private static Func<int> b => throw new NotImplementedException();
        internal static void test()
        {
            var x = () => { a(); b(); }; // this is a valid expression. x is Action.
            var y = () => { a(); b() }; // this is currently invalid due to the missing semicolon. were it valid, y would be an int.
        }
    }

a semicolon would make a big difference in the inferred type. also this would look quite odd to me.

@333fred
Copy link
Member

333fred commented Sep 24, 2024

@ltagliaro I don't see how that would break the grammar. It would simply be an error.

@vladd
Copy link

vladd commented Sep 24, 2024

The use of curly braces for expression blocks would break the current grammar in several cases

What cases are these?

This one looks suspicious:

$$$"""{{{{C c = new(); c}}}}"""

@ikegami
Copy link

ikegami commented Sep 24, 2024 via email

@ltagliaro
Copy link

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?

block_expression
    : '{' statement+ expression '}'
    ;

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.

@333fred
Copy link
Member

333fred commented Sep 24, 2024

The use of curly braces for expression blocks would break the current grammar in several cases

What cases are these?

This one looks suspicious:

$$$"""{{{{C c = new(); c}}}}"""

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.

@vladd
Copy link

vladd commented Sep 24, 2024

@333fred
As of now, $$$"""{{{1}}}""" produces "1", $$$"""{{{{1}}}}""" produces "{1}", and I expected $$$"""{{{{C c = new(); c}}}}""" to produce the same result as $$$"""{{{new C()}}}""" (akin to the first example), because {C c = new(); c} is the same as new C().

@vladd
Copy link

vladd commented Sep 24, 2024

@333fred
Ok, here is another attempt:

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 {null} to X, or a collection property initializer?

@333fred
Copy link
Member

333fred commented Sep 24, 2024

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.

@alrz
Copy link
Member

alrz commented Sep 26, 2024

Is it a direct assignment of the expression {null} to X

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 new C { X = ({ null }) }.

@BN-KS
Copy link

BN-KS commented Oct 1, 2024

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.

@AderitoSilva
Copy link

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 is pattern for that, because I can't think of another way. Although this does the job, it translates to unnecessary operations in the output just for the sake of the developer's taste, which is not pretty. This would not be necessary if we had a keyword to access the previous expression's value, like value, similar to property setters.

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 new(), like this:

MyObject obj = new() { // Empty initializer } { // Expression block }

In the shown example, you would need to include an empty initializer ({ }) before the expression block. Alternatively, if you don't want to specify an empty initializer, you could isolate the initialized object in parentheses, like:

MyObject obj = (new MyObject()) { // Expression block }

Please let me know what do you guys think of treating expression blocks as anonymous local methods.

@HaloFour
Copy link
Contributor

@AderitoSilva

What syntax are you suggesting? That C# support IIFE, or that expression blocks are lambda method bodies?

@AderitoSilva
Copy link

@AderitoSilva

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

@HaloFour
Copy link
Contributor

@AderitoSilva

I think that runs into the same issues as the following already means something different:

{ return 10; }

@AderitoSilva
Copy link

@HaloFour

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

@HaloFour
Copy link
Contributor

HaloFour commented Oct 22, 2024

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

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.

@AderitoSilva
Copy link

@HaloFour

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

@ayende
Copy link

ayende commented Oct 23, 2024

What is the meaning of:

int i = {
   if (DateTime.Today.DayOfWeek == DayOfWeek.Sunday)
         return 5;

   return 6;
}

And how do I return from the method inside the expression?

@Thaina
Copy link

Thaina commented Oct 23, 2024

I have been propose :{} since #249

var i = :{
   if (DateTime.Today.DayOfWeek == DayOfWeek.Sunday)
         return 5;

   return 6;
}

Even {} is valid block in C# we just need anything to mark it that it was expression block

And 249 state. What we really want is anonymous immediate local function. We want local function that would be anonymous and just execute immediately

@AderitoSilva
Copy link

@ayende
If expression blocks were thought of as immediate lambda functions, that block would mean the variable i would be 5 or 6 depending on the day of the week. You would not be able to return from the method when inside the expression block, just as you could not from inside a lambda function. That's the way I see it.

@AderitoSilva
Copy link

AderitoSilva commented Oct 23, 2024

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 is pattern. For example, in my previous example, I do this with current C#:

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 is pattern just to be able to use the left operand of the ?: operator as the variable s, so I can pass s to the lambda as a parameter. Suppose we had an operator just for doing this, by taking its left operand, assign it to a variable name and return its right operand. For example, this:

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 is string s, but returning the expression on the right instead of bool. The previous example would be 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 Function<string>, and has no parameters; it just processes s and returns it.

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.

@HaloFour
Copy link
Contributor

@AderitoSilva

See: #4748

@vain0x
Copy link

vain0x commented Oct 23, 2024

@AderitoSilva

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 return keyword to break out of an expression block is inconvenient. Returning from method out of an expression block, a convenient use-case of expression blocks, would be unable. (I mean a potential extension. This proposal restricts return.)

@BN-KS
Copy link

BN-KS commented Oct 23, 2024

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:
https://medium.com/go-with-flutter/double-d-triple-dots-in-flutter-dbe2a42dd464#:~:text=Double%20dots(..)&text=%E2%80%9C..%20%E2%80%9Dis%20known%20as,to%20write%20more%20fluid%20code.

Example:

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

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.

@timcassell
Copy link

timcassell commented Oct 23, 2024

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 |> p ${ ... }.

@HaloFour
Copy link
Contributor

HaloFour commented Oct 23, 2024

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.

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