Cascade expressions with 'then' and 'it'. #2100
Replies: 58 comments
-
In the following: Children.Add(new Entry()
then Text = new Text()
then Placeholder = await GetPlaceholderFromServerAsync(Text)); Does the second |
Beta Was this translation helpful? Give feedback.
-
It applies to the
|
Beta Was this translation helpful? Give feedback.
-
I actually uncertain how you grammatically could ensure that. Could you clarify? If 'then' is allowed after 'new entry()' how do you make the second 'then' not bind to |
Beta Was this translation helpful? Give feedback.
-
Still working on the formal grammar changes needed. Informally, the keyword I'm a little new to the C# grammar definition, so forgive me if this is off, but here's a rough go at it. The definition of
The part I'm struggling with is the definition of My work in progress:
|
Beta Was this translation helpful? Give feedback.
-
if you define things as: cascade-expression (new):
expression "then" cascade-statement then, in your example, |
Beta Was this translation helpful? Give feedback.
-
Or, if you're trying to make it so that post- |
Beta Was this translation helpful? Give feedback.
-
Yes, multiple thens are allowed, but the intent is that a then cannot be in a cascade-statement at the top level (it can be in a block or in parentheses), so when the second then is encountered it finishes off the cascade-statement, then the cascade-expression, then starts a new cascade expression using the previous one as the target expression. |
Beta Was this translation helpful? Give feedback.
-
So i could not do:
? |
Beta Was this translation helpful? Give feedback.
-
No, you can do that. It's possible to pare down cascade-statement significantly if needed and still retain the usefulness of this syntax. For example, at a bare minimum you could just support
Too many semicolons and braces for the simpler use cases. But you could add just a few additional cases, and use
Note that cascade-statement is its own definition and so it doesn't interfere with any other existing definition in the statements section. |
Beta Was this translation helpful? Give feedback.
-
Ok. I guess i need to see what your grammar would actually be. I'm not quite getting it yet. |
Beta Was this translation helpful? Give feedback.
-
The problem here is the recursion of:
|
Beta Was this translation helpful? Give feedback.
-
Yes, I see the problem. Fundamentally the problem with my proposal is the ambiguity between the start of a new cascade expression and the continuation of an existing one. Additionally, although you haven't mentioned it, the
|
Beta Was this translation helpful? Give feedback.
-
I thnk i've lost what problem we're actually trying to solve, and how this makes things better than today. |
Beta Was this translation helpful? Give feedback.
-
Yep, it's important to remember the motivation for this suggestion. I explained a little bit of it in the original post, but I'll try to clarify as best I can. In this case, I would like to make declarative code less painful on C#. "declarative" is a little ambiguous here, but what I mean by declarative code is any code that constructs a complex object/function tree. Could be a UI widget tree, or a build system, or any kind of pipeline or workflow API, such as one you might see in AI or machine learning or state machines, and so on. Declarative code in C# is painful because most declarative code is actually a mix of declarative and imperative code. It's the mix that's painful in C# - anytime you need to mix a little imperative code into a declarative section of code in C#, you have to use cumbersome aids - temp variables, helper methods, extension methods, or redesign your API (if it is indeed your API to redesign) to be more "fluent". This is because you can't execute code on an unnamed object in your object tree before inserting it into the object tree - not without a temp variable or a helper method or an extension method. Or, by creating a perhaps overengineered "Builder" API that is "fluent". Try this excercise to see what I mean. Find some of your own production Xamarin.Forms or WPF or UWP UI written in XAML (or find some on github) and TRY to convert the XAML to pure C#. BUT, you're not allowed to use: temp variables, helper methods, or extension methods. Your entire UI has to be defined after a When constructing an object in C# without a name for that object to reference it by (as is often done in declarative programming), you have basically three options for initializing that object: constructor parameters, property/field initializers, and using a helper method (including extension methods). If you want to assign a field to that instance, call a method on that instance, reference another object in the tree, use an indexer on that instance, or call another method on the instance itself, your ONLY option is a helper method or move that object initialization above the object tree as a temp variable. If you need to reference a parent object or another object in the tree, those too would need to be moved to temp variables. Until the entire tree is flattened out as temp variables and then constructed at the end. In other words, a real mess. This proposal addresses that problem by introducing a way to insert some code after an object initialization or other expression without breaking out of the object tree expression via a helper method or temp variable. It improves the readability of the code by keeping related code together, as opposed to somewhere above in a temp variable or in a helper method. It reduces the need to design and implement cumbersome fluent builder APIs to make declarative programming tolerable. |
Beta Was this translation helpful? Give feedback.
-
None of that seems cumbersome. And it doesn't seem any more cumbersome then using some new construct here AFAICT.
I don't see why that's interesting, or why i would even care to do that. It's like having someone come to your proposal and saying "ok... so cascade expressions are nice, but you're not allowd to used them for Xaml." Why would i be interested in framing things that way. It would be akin to someone saying "yeah, cascade exprs are great, if you're on C#1". But we're not. We're on C# 8 and there's tons of options to improve code and make it easier to write. Saying we need another feature here because you've arbitrarily carved out all the improvements since 1.0 seems super strange to me.
I don't see any mess here. :)
I don't see how it's more readable. Heck, Roslyn itself has tons of lines like this, and it's pretty easy to grok.
Why does a variable hurt readability? I honestly ask that as someone who has experience with a huge swatch of languages across may paradigms. I think this is one of the first times i've heard this being stated. And it's not like i haven't heard people talking about this space a lot. Indeed, it's often the reverse where people tend to find things confusing as jamming everything into one massive expression can actually make it harder to tell what's going on. Variables actually address this in a very natural way by allowing one to break up ideas that are too large and to operate on entities that are named effectively to abstract over that complexity and allow you to just work with something representing it instead.
I don't see anything intolerable about declarative programming. The difference is that i don't see the need to make "everything an expression". I've actually used languages that have really pushed for that sort of thing, and they're jsut about as tolerable to use as others are. |
Beta Was this translation helpful? Give feedback.
-
I'm not quite understanding the problem. While removing white spaces makes it a little harder to read (as would removing white space in many situations), the precedence is always a simple chain from left to right. Each If you wanted the I wouldn't expect the |
Beta Was this translation helpful? Give feedback.
-
That's where the problem lies. There's no way for the compiler to know that the "previous section" is complete. As you say you have to introduce parenthesis to allow cascading on child expressions, something that C# has never required. It turns ugly fast. It doesn't even look good in Dart. |
Beta Was this translation helpful? Give feedback.
-
How would the compiler struggle to know when a section is complete? If Dart can do it, and I can visually/manually do it, its technically feasible, at least that can be confirmed. Unless C# has something different, that throws a spanner in the works, that I'm not seeing? |
Beta Was this translation helpful? Give feedback.
-
Because any and all operators would apply to the subexpression first. That's how all C# syntax works today, including the dot operator. Could C# be changed to make |
Beta Was this translation helpful? Give feedback.
-
The issue with the original proposal wasn't related to whitespace specifically but with the recursion of the expression definition:
Except that because I think Dart gets around this by limiting syntactically what can be to the left of the |
Beta Was this translation helpful? Give feedback.
-
I think it should be noted too that the problems I'm describing above could also be solved with support of expression blocks, as in other languages, though I think implementing expression blocks in C# would cause all sorts of havoc to the grammar. It would look something like this:
C# doesn't even support immediately executing a lambda as an expression:
|
Beta Was this translation helpful? Give feedback.
-
@adamped asked
I'd suggest reuse/expansion of the syntax we already have in C# for object initialization expressions: permit them to happen after any expression, instead of only after object construction, and slightly extend the statements permitted within. Here's what some of the examples already given would look like ... ... though this one is just object initialization anyway ... new Entry()
{
Text = "Something",
Color = Color.Black,
TextChanged += Entry_TextChanged
} new IconButton(...)
{
Pressed.Subscribe(OpenFilters)
} override Widget Build(BuildContext context)
=> new Scaffold(
appBar: new AppBar(
title: new Text(""),
centerTitle: true,
actions: new Widget[]
{
new IconButton(icon: new Icon(Icons.Filter))
{
Pressed.Subscribe(OpenFilters)
},
new IconButton(icon: new Icon(Icons.Notifications))
{
Pressed.Subscribe(OpenNotifications)
},
new IconButton(icon: new Icon(Icons.Account))
{
Pressed.Subscribe(OpenAccount)
}
}
),
body: BuildBody()
); I've assumed that To be clear - I still don't see a need for writing code in this style. If I was intent on writing obfuscated and hard to maintain code, designed to frustrate future maintenance developers, this is exactly what I'd do. But, if wiser minds decide that C# needs to support this development style, let's do it in a way that's consistent with what's gone before, retaining the feel of the language. |
Beta Was this translation helpful? Give feedback.
-
I thought about extending the object initialization expressions, which I agree would suit the language better. It removes the ability to use it anywhere other than object initialization, but I'm not thinking of too many use cases where that would be much of a problem. |
Beta Was this translation helpful? Give feedback.
-
@theunrepentantgeek I actually kind of like this suggestion, though this:
Would probably be ambiguous in the grammar? And without that, you can't do composition with helper functions:
Another minor issue would be an inability to pass the target expression object to another method, one of the main use cases. Or would this be allowed (using declaration syntax)?
But this might be ambiguous since TrackOutOfBounds should be parsed as a member of the object being constructed to stay consistent, right?
That's OK. When it comes to programming styles and idioms, different developers often have different opinions on which style is easier or more difficult to read and maintain. For instance some developers swear by more strict or pure functional programming styles over an object oriented approach, while others think functional programming can be difficult to read. Some mix and match both styles where appropriate. I think it's the same with imperative vs declarative. One of the benefits to using a programming language over a markup language is the flexibility to choose which style is appropriate or to blend both styles in a way that makes sense to you. As for me personally, I'm coming from doing a lot of work with markup languages - mostly UI layouts on Android, WPF, Xamarin.Forms, and the web with HTML. It works well enough, but after seeing what's possible with frameworks like React and Flutter, I actually think the flexibility of using a programming language is a net positive in terms of benefits over a markup language. |
Beta Was this translation helpful? Give feedback.
-
Kotlin is really nice with its |
Beta Was this translation helpful? Give feedback.
-
@gulshan - could you please comment with an example on how you might think this would look in C#. I'm still interested in cascading expressions, but still some discussion to be had on possible best approaches :) |
Beta Was this translation helpful? Give feedback.
-
@adamped I really don't know how the Kotlin constructs can be translated into C#. Kotlin has several different constructs, and they altogether makes it cleaner in my opinion. I want to mention some of them-
With all this, Kotlin can have something like (Kotlin don't use val entry = Entry().apply{
Text = "Something"
Color = Color.Black
TextChanged += Entry_TextChanged
} Which will be in single line- val entry = Entry().apply{ Text = "Something"; Color = Color.Black; TextChanged += Entry_TextChanged } More Kotlin details can be seen at- https://kotlinexpertise.com/coping-with-kotlins-scope-functions/ The closest I think we can have in C# is with #91 - var entry = new Entry().Apply({
@.Text = "Something";
@.Color = Color.Black;
@.TextChanged += Entry_TextChanged;
}); But this still doesn't feel as clean as Kotlin. |
Beta Was this translation helpful? Give feedback.
-
The example given would use a lambda where it is completely unnecessary, just to provide some syntax sugar. Given the overhead of a lambda (every lambda creates at least one allocation, and a function call at minimum), I don't think it's sensible using lambdas purely as syntactic sugar. |
Beta Was this translation helpful? Give feedback.
-
How Kotlin handles it- https://kotlinlang.org/docs/reference/inline-functions.html |
Beta Was this translation helpful? Give feedback.
-
Kotlin used lambda with receiver with inlining. Lambda with receiver is a nice feature for creating powerful construction, like : with, also, run, apply, use , etc( see kotlin specification). |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Other similar issues:
then
keyword, but the meaning is completely different).Motivation
Declarative code can be slightly painful in C#, often requiring the use of temporary variables. Named constructor parameters, and initialization syntax, helps, but it is limited to just those for object initialization. Notably missing are methods that take the object as an argument and methods on the object itself. In either of those :
Also, if you have an object tree where you want a reference to part of the object tree, you also must make a temp variable, which breaks up the object tree expression, harming readability:
Note how @VincentH-Net's CSharpForMarkup library has to introduce an extension method to address some of these issues:
However, these solutions require hand-crafted Fluent APIs from extension methods, which are specific to the object tree types. This proposal introduces a general purpose solution to these problems and improves C#'s usability with declarative code.
Proposal
This proposal introduces two new keywords,
then
andit
.it
stands as a placeholder for an unnamed value in the context where one exists, as in this proposal, but could be used as such in other proposals (e.g. a hypothetical streamlining of lambda expression syntax for lambdas with one argument, as in scala or kotlin).then
is a keyword that follows an expression (eagerly evaluated) in the form ofexpression then statement
, which itself becomes an expression (thus chaining is possible:expression then statement1 then statement2
is equivalent to(expression then statement1) then statement2
). Such an expression would be called a "cascade".The meaning of a cascade is as follows: first the expression to the left of
then
is evaluated, then the statement to the right ofthen
. The value of the cascade expression is the value of the expression after the statement in the cascade is executed. Within the statement in a cascade expression, all accessible members of the expression object can be referenced without qualification (except for operators and indexers), unless there is a conflict with a name in the local scope within which the cascade expression lives:When it appears within a statement inside a cascade expression, the keyword
it
stands in for the value of the expression to the left of thethen
keyword. However, use ofit
explicitly is not necessary unless there is a conflict with a name in the local scope within which the cascade expression lives, or when using operators or indexers on the value of the expression:Name conflicts are resolved with local variables/members taking precedence over the members of the target-expression object in a cascade expression. If you take a look at the example above, note how
Length
is a property of bothFoo
andint[]
. In this case, just typing Length references the Foo.Length, whereasit.Length
references the target expression object'sLength
property. In this case, the target expression object is an array of int.Statements in a cascade expression can use
async/await
:Cascade expressions can be chained. They are evaluated left-to-right:
Beta Was this translation helpful? Give feedback.
All reactions