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 Alternative for Statements #5402

Closed
alrz opened this issue Sep 23, 2015 · 35 comments
Closed

Proposal: Expression Alternative for Statements #5402

alrz opened this issue Sep 23, 2015 · 35 comments

Comments

@alrz
Copy link
Contributor

alrz commented Sep 23, 2015

Following the proposals #254, #3718, #5154, #5143 and others, it would be a good idea to have an expression alternative for other constructs as well.

Syntax

expression:
 ...
if-expression
try-expression
iterator-expression
using-expression

Note: It is not proposed that these be added to the set of syntax forms allowed as an expression-statement.

if-expression:
if ( boolean-expression ) expression

Note that if-expression cannot return and else expression seems redundant because of the existing ternary operator.

try-expression:
try expression

try-expression returns a null in case of an exception, so you can use null-coalescing operator.

iterator-expression:
foreach-expression
for-expression
while-expression
do-expression

foreach-expression:
foreach ( local-variable-type identifier in expression ) expression

for-expression:
for ( for-initializeropt ; for-conditionopt; for-iteratoropt) expression

while-expression:
while ( boolean-expression ) expression

do-expression:
do expression while ( boolean-expression )

Note that iterator-expression cannot return values unless it's used in expression-bodied iterators.

using-expression:
using ( resource-acquisition ) expression

Additional syntaxes to be used with statement expressions including:

anonymous-method-expression:
 ...
delegate => expression

accessor-body:
 ...
=> expression ;

Some of the general expressions mentioned above should be narrowed to prevent polluted code.

@gafter
Copy link
Member

gafter commented Sep 24, 2015

This seems more like a concept than a proposal, as there are more questions than answers in the suggestion.

@aluanhaddad
Copy link

I find this proposal strange because you mention immutability as a motivation but several of your examples explicitly involve mutable state.
Also, many of the examples themselves seem redundant with existing language features.
For example:

// foreach, conditional —  basically list.Where(...).Select(...);
var result = foreach(var item in list) => if(item.Condition) => new Foo(item);

Why would you want this at all? Why not just write

var result = from item in list where item.Condition select new Foo(item);

which is more readable, more concise, and already exists.

And then you have both this:

IEnumerable<int> result = for(var i = 1; i <= 10; ++i) => i;

and this:

// block body
var result = for(var i = 1; i <= 10; ++i) => { 
    var item = i.ToString();
    yield return item;
};

What does this mean? It suggests that if there are braces after the => the scope is expanded into an implicitly defined and implicitly invoked anonymous function which automatically encloses the control construct, but if there are no braces after the => the scope is the construct's body. That seems extremely confusing.

@alrz
Copy link
Contributor Author

alrz commented Oct 4, 2015

@aluanhaddad As I said, loop expressions can be easily written in terms of LINQ methods/expressions. But for using and try catch you have to define a separate method. For example in a match expression what if you want to use a using or try catch in the case body? This can be easily done with an expression alternative. Ix (from Rx) have methods like Using and Catch for this purpose and you can create your data flow with them but only if you are working with an IEnumerable<>. Besides, while it seems redundant, it helps to avoid the need for defining a new method where you just need an expression instead of a statement.
About braces, same pattern is used by lambda expressions; if there are braces you have a block body otherwise it has to be an expression. And yes it would be translated to an implicitly defined and implicitly invoked anonymous function because the value should be created somewhere. so the above translates to:

static IEnumerable<string> F() {
    for(var i = 1; i <= 10; ++i) { 
        var item = i.ToString();
        yield return item;
    }
}

...

var result = F();

Which is nice, because you don't need to define a method wherever you just want a value and have to use a for, foreach, while, using or try catch (if is excluded because it already has an expression form (:?) but still it would make sense in a loop expression when you haven't an else, or when you need it to be a block-body that returns.
My inspiration for this proposal is from F# where everything is an expression even assignments as a whole e.g. let val = value in expression.

@aluanhaddad
Copy link

I think you are conflating yield in F# with yield return in C#.

@alrz
Copy link
Contributor Author

alrz commented Oct 10, 2015

@gafter @aluanhaddad I just figured out my mistake about loop expressions: they cannot return values unless they are used in iterators. So there would be no implicitly defined and implicitly invoked anonymous function. Please review the new specification.

@gafter
Copy link
Member

gafter commented Oct 10, 2015

What is expression-or-block? If it supports a block, what is it's value and type?

@alrz
Copy link
Contributor Author

alrz commented Oct 10, 2015

@gafter It follows anonymous-function-body rules. so if it supposed to return a value, one may use return statement.

Action f = () => try => this.VoidMethod() catch => this.VoidMethod();
Action f = () => try => { this.VoidMethod(); } catch => { this.VoidMethod(); };
Func<int> g = () => try => this.IntMethod() catch => this.IntMethod();
Func<int> g = () => try => { return this.IntMethod(); } catch => { return this.IntMethod(); };

The lambda expression body remains in the expression context in all cases.

However, if cannot return. else expression seems redundant because of the existing ternary operator.

@aluanhaddad
Copy link

@alrz I see, but I think it would make more sense, in the context of this proposal, to allow loops to return values but to disallow iterators in loop expression block bodies. What I was pointing out that the return type didn't make sense. It would have been something like IEnumerable<IEnumerable<int>>.

That said, I do not think any of this is a good idea. Initially I found the exception handling capabilities of this proposal to be compelling, but after seeing these examples

Action f = () => try => this.VoidMethod() catch => this.VoidMethod();

Func<int> g = () => try => this.IntMethod() catch => this.IntMethod();

I think it could well encourage overgeneralized exception handling for the sake of brevity.

@alrz
Copy link
Contributor Author

alrz commented Oct 11, 2015

@aluanhaddad

I think it could well encourage overgeneralized exception handling for the sake of brevity.

You think checked exceptions in Java (as the opposite case) encourage developers to really catch the exceptions or explicitly bubble them up with throws? On the contrary, in Java using a general catch is more common than C#. Just because Java forces to explicitly handle the exceptions doesn't mean that people really do!

I think this totally depends on developers, if that is not the case, a general catch should have never been introduced to the language, "because it encourages to overgeneralized exception handling", right?

allow loops to return values but to disallow iterators in loop expression block bodies. What I was pointing out that the return type didn't make sense. It would have been something like IEnumerable<IEnumerable<int>>.

I didn't quite get that, can you give an example?

@aluanhaddad
Copy link

@alrz It is not about checked vs unchecked exception, it is about having catch clauses that catch everything. I'm not saying catch {...} is never appropriate, but it often masks errors.

I didn't quite get that, can you give an example?

You originally posted this example:

IEnumerable<int> result = for(var i = 1; i <= 10; ++i) => i;

and this example:

IEnumerable<string> result = for(var i = 1; i <= 10; ++i) => { 
    var item = i.ToString();
    yield return item;
};

But for the proposal to make any sense, one of them must be wrong.
Either the result type of the first is int, or the result type of the second is IEnumerable<IEnumerable<string>>, or the yield keyword should have been omitted.

@alrz
Copy link
Contributor Author

alrz commented Oct 11, 2015

@aluanhaddad

the result type of the first is int

That would not make sense. Because i is not a single int, it will evaluate several times with different values. same goes with the second example. I gave up this syntax when I realized that for expressions like that can be used only within lists or sequence expressions in F# not the function body (because F# has nothing like iterator methods). Actually I trimmed list comprehensions from #5865 because the same arguments about for expressions returning values goes "why don't we use LINQ instead?" And that's correct. LINQ already covered this. Examples you mentioned are equivalent to [for i = 1 to 10 do yield i] and [ for i = 1 to 10 do let item = i.ToString() in yield item ] in F# which both return a list.

@aluanhaddad
Copy link

Part of your original proposal was also block expressions. The second example suggested that you could use an iterator body as a block expression body meaning that the body which as you say would be evaluated several times, would evaluate to a new iterator for each iteration of the loop. So the second example would have been a loop which produced multiple iterators hence the type of the loop as an expression.

@alrz
Copy link
Contributor Author

alrz commented Oct 11, 2015

@aluanhaddad now I see what you're talking about. with yield return I meant return from for block not enclosing method. Anyway, I gave up on that syntax because it will be confusing and complicated to implement.

@alrz
Copy link
Contributor Author

alrz commented Oct 13, 2015

@gafter As @GSPP pointed out, it makes sense to have switch expressions (actually it was part of this proposal, but I have been excluded it in favor of match). This makes introducing match syntax totally unnecessary.

I updated the original post, however, I wasn't sure about precedence of these expressions so I just omitted augmented existing rules for now.

@gafter
Copy link
Member

gafter commented Oct 13, 2015

Do you have semantic rules in mind for this? e.g. what is the type of an if-expression or an iterator expression? Which statements are allowed in a block-expression, and which are not? What are the semantics of statements embedded in a block-expression? What are the precedence rules?

It appears that this would make the language terribly ambiguous, for example

if (e) { M(); } // expression or statement?
.ToString(); // Oh, it was an expression. Is .ToString applied to the whole if expression or the block?

@alrz
Copy link
Contributor Author

alrz commented Oct 13, 2015

@gafter

It appears that this would make the language terribly ambiguous

It wasn't, with previous syntax with the => operator, since you removed it from match for the reason that "it doesn't really relate in any way to a method body" I did it here too. As it turns out, I should not.

what is the type of an if-expression

if-expression would be void or never. the use case for this is in void-returning methods like

Array.ForEach(array, item => if(item % 2 == 0) => Console.WriteLine(item))

Which statements are allowed in a block-expression, and which are not?

statement-list implies that any statement would be allowed in a block-expression because, the point is to allow non-expression statements or ones that haven't an expression alternative to be used there. I don't think that it would be a good idea to have an expression form for like any statement that exist in the language (or is it?).

What are the precedence rules?

As I said, I wasn't sure about precedences, you made match a relational-expression I didn't know the reason so I just decided to don't make any decision on this matter.

Is .ToString applied to the whole if expression or the block?

First, if-expression doesn't return any value, so you can't do that. The following

{ M(); }.ToString();

is very similar to an object initializer. since new Foo { ... }.ToString() is possible. so dot operator has a lower precedence than block-expression.

@gafter
Copy link
Member

gafter commented Oct 13, 2015

I don't get it. How is an if-expression any more convenient than an if-statement?

So we would be allowed to break and continue from an enclosing loop, and return from the enclosing method in a block-expression?

@alrz
Copy link
Contributor Author

alrz commented Oct 13, 2015

@gafter the purpose is to keep the procedure in the expression context. that's all. break and continue wouldn't be a problem, but return depends on your comment in #5154. Will we ever need to return from an expression at all?

@gafter
Copy link
Member

gafter commented Oct 13, 2015

@alrz There are other ways to "return from an expression" - for example, goto the last expression in a block - but hijacking the meaning of return in an expression prevents writing APIs that act as user-defined control constructs. Unfortunately the fact that you use {} for block-expression makes a statement lambda (which necessarily hijacks return) and an expression lambda (which need not hijack return) syntactically indistinguishable. (In fact, that is an incompatibility with existing behavior)

@alrz
Copy link
Contributor Author

alrz commented Oct 13, 2015

@gafter

for example, goto the last expression in a block

I don't understand. you mean from outside of the block (expression block?)? Is that possible while label is not in the scope?

about expression lambdas, well, there are two options left;

  • Allow expression-block only in contexts that it would make sense, like other expression statements. (basically expression-or-block in the previous syntax)
  • Use a different syntax for block-like expressions, e.g. something like C++ style comma operator but with a semicolon (cause comma would be ambigious with tuples) see #3718 comment. I don't like this though.

btw, there is an "expression block" in Scala and it works. What I'm missing here? This is kind of problems that you were trying to solve by choosing parens for match or it's just a preference?

@gafter
Copy link
Member

gafter commented Oct 13, 2015

What I meant is that you can "return early" from inside a block by using a goto to its final expression.

Indeed, there is an expression-block in Scala and it does not hijack the meaning of return.

We already toyed with block-expressions in C# 6 and the syntax we came up with was ( statements expression ) but in our prototype we restricted the statements to either a local declaration statement or an expression-statement. There is no need for a "statement separator" in this syntax as statements already come with their own terminators.

@alrz
Copy link
Contributor Author

alrz commented Oct 14, 2015

that wouldn't be possible because goto-statement is not part of expression-statement.

syntax we came up with was ( statements expression )

this is just great, and it's consistent with parenthesised-expression. I love to see this in the next version along with match. More I think about parenthesised match it makes more sense too. but the fact that match will often break into multiple lines, gives it a SQLish look IMO.

@alrz
Copy link
Contributor Author

alrz commented Oct 14, 2015

@gafter block-expression aside, I replaced => with : in expression statements to be explicitly distinguishable from a lambda expression at first sight (however, this makes switch-expression ugly as hell , so it can be safely excluded in favor of match). If that doesn't help with ambiguity problem, then this will not going to happen.

@gafter
Copy link
Member

gafter commented Oct 14, 2015

@alrz Re

that wouldn't be possible because goto-statement is not part of expression-statement.

It could be extended by relaxing the set of statement forms allowed within it.

On the other hand, if you co-opt the meaning of return there is no possible future extension in which it can be restored.

@alrz
Copy link
Contributor Author

alrz commented Oct 14, 2015

@gafter since the syntax you have mentioned was well designed enough I removed expression-block from the suggestion. So there would be no return in this grammar.

@gafter
Copy link
Member

gafter commented Oct 14, 2015

You introduced conjunctive-pattern and disjunctive-pattern, but those productions aren't used anywhere. If you intend them to be examples of pattern, then your grammar is ambiguous.

@alrz
Copy link
Contributor Author

alrz commented Oct 15, 2015

@gafter I just wanted to show how would multiple-case switch work. They're not part of this proposal actually.

@gafter
Copy link
Member

gafter commented Oct 15, 2015

x is I1 and x is I2 or x is I3

Is this a conjunction of a disjunction or vice versa? Your grammar is ambiguous.

@alrz
Copy link
Contributor Author

alrz commented Oct 15, 2015

@gafter Yes, I just figured out. Fixed.

@gafter
Copy link
Member

gafter commented Oct 15, 2015

@alrz I still don't see conjunctive-pattern or disjunctive-pattern used in (the right-hand-side of) any syntactic productions, so I don't see how they can be used in a program, or what their precedence would be.

@alrz
Copy link
Contributor Author

alrz commented Oct 15, 2015

@gafter They are defined under pattern so they would be used in match, switch or with is operator. (apparently my post didn't get updated before)

@lachbaer
Copy link
Contributor

lachbaer commented Feb 9, 2016

Instead of using delegate => expression; one should use the corresponding llambda expression, as that's what they are for. Not introducing a new (500th) way to define delegates and anonymous functions will keep the language cleaner and nicer.

static Action Foo;
static Func<int, int> Bar;
static void Main(string[] args)
{
    Foo = () => Console.WriteLine("Foo Lambda");
    Foo();
    Foo = delegate { Console.WriteLine("Foo Delegate"); };
    Foo();
    // next one is the (by me declined) proposal:
    Foo = delegate => Console.WriteLine("Why would this statement really help?");
    Foo();

    Bar = (x) => x;
    Console.WriteLine("Bar Lambda: " + Bar(5).ToString());

    Bar = delegate(int x) { return x; };
    Console.WriteLine("Bar Delegate: " + Bar(5).ToString());

    Console.ReadLine();
}

@alrz
Copy link
Contributor Author

alrz commented Feb 11, 2016

Closing in favor of dedicated issues for each subject discussed in this thread.

@alrz alrz closed this as completed Feb 11, 2016
@masaeedu
Copy link

@gafter Is there an issue or design discussion tracking the "block expressions" you mentioned earlier? My primary use case is the ability to wrap void returning expressions into a larger Unit returning expression, which currently doesn't seem to be possible in the language.

@alrz
Copy link
Contributor Author

alrz commented Aug 24, 2016

@masaeedu #6182

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

6 participants