dotnet / csharplang

The official repo for the design of the C# programming language
11.52k stars 1.03k forks source link

[Proposal]: Compound assignment in object initializer and `with` expression #5176

Open CyrusNajmabadi opened 3 years ago

CyrusNajmabadi commented 3 years ago

Compound assignment in object initializer and with expression

Summary

Allow compound assignments like so in an object initializer:

var timer = new DispatcherTimer {
    Interval = TimeSpan.FromSeconds(1d),
    Tick += (_, _) => { /*actual work*/ },
};

Or a with expression:

var newCounter = counter with {
    Value -= 1,
};

Motivation

It's not uncommon, especially in UI frameworks, to create objects that both have values assigned and need events hooked up as part of initialization. While object initializers addressed the first part with a nice shorthand syntax, the latter still requires additional statements to be made. This makes it impossible to simply create these sorts of objects as a simple declaration expression, negating their use from things like expression-bodied members, switch expressions, as well as just making things more verbose for such a simple concept.

The applies to more than just events though as objects created (esp. based off another object with with) may want their initialized values to be relative to a prior or default state.

Detailed design - Object initializer

The existing https://github.com/dotnet/csharplang/blob/main/spec/expressions.md#object-initializers will be updated to state:

member_initializer
-    : initializer_target '=' initializer_value
+    : initializer_target assignment_operator initializer_value
    ;

The spec language will be changed to:

If an initializer_target is followed by an equals ('=') sign, it can be followed by either an expression, an object initializer or a collection initializer. If it is followed by any other assignment operator it can only be followed by an expression.

If an initializer_target is followed by an equals ('=') sign it not possible for expressions within the object initializer to refer to the newly created object it is initializing. If it is followed by any other assignment operator, the new value will be created by reading the value from the new created object and then writing back into it.

A member initializer that specifies an expression after the assignment_operator is processed in the same way as an assignment to the target.

Detailed design - with expression

The existing with expression spec will be updated to state:

member_initializer
-    : identifier '=' expression
+    : identifier assignment_operator expression
    ;

The spec language will be changed to:

First, receiver's "clone" method (specified above) is invoked and its result is converted to the receiver's type. Then, each member_initializer is processed the same way as a corresponding assignment operation assignment to a field or property access of the result of the conversion. Assignments are processed in lexical order.

Design Questions/Notes/Meetings

Note: there is no concern that new X() { a += b } has meaning today (for example, as a collection initializer). That's because the spec mandates that a collection initializer's element_initializer is:

element_initializer
    : non_assignment_expression
    | '{' expression_list '}'
    ;

By requiring that all collection elements are non_assignment_expression, a += b is already disallowed as that is an assignment_expression.

--

There is an open question if this is needed. For example, users could support some of these scenarios doing something like so:

var timer = new DispatcherTimer {
    Interval = TimeSpan.FromSeconds(1d),
}.Init(t => t.Tick += (_, _) => { /*actual work*/ }),

That said, this would only work for non-init members, which seems unfortunate.

LDM Discussions

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-09-20.md#object-initializer-event-hookup

najak3d commented 2 years ago

My purpose here was mostly wanting to make sure this issue was heard -- and it was. I don't think I have any other use-cases that would help further my argument. Therefore I think the "answer is NO" is accurate.

I do bring up issues about the ecosystem in which I find myself, and want to make sure you understand that in the space where Xamarin-once-dominated, we are now quickly losing ground -- which means C# is also losing ground in this arena.

We are actively trying to regain that ground, and I wanted to make sure that the C#-team is aware that one of the "necessary things we have to do, to compete" requires us to create a Hack, to alter C# syntax to mimic what the community wants. So now you are aware (if not already), and I've made some noise. I appear to be mostly alone here (or alone enough) to warrant dismissal of my request.

Maybe in the future, more will step forward, and this concept will gain momentum. But I'm not counting on it. It's really non-essential to my mission. We have a way to move forward with C# "as is", and that is what we are doing.

CyrusNajmabadi commented 2 years ago

to alter C# syntax to mimic what the community wants.

If the community wants this, the community needs to show the real use cases that this would really address. :-)

Therefore I think the "answer is NO" is accurate.

You are incorrect. That is not the answer, and it's wrong to keep stating that.

The answer has been: this needs more if a defense and real answers as to why the existing options are not suitable and why this is the right 'solution' to an actual 'problem' that's been well explained.

I've responded to many/all your points, but I've still been waiting on real answers to many questions on my part. Without that information I'm not seeing enough to demonstrate why this is important and should get focus over all the rest of the things we are working on.

Thank you :-)

najak3d commented 2 years ago

I'll come back soon, with a few dumps of C# UI.Composition examples in Avalonia, so that you might witness the rightful "appeal" to this notation. Not that it will change your mind, but may increase your awareness.

I believe the "init-block" (for easy stuff/setters) followed by the extension-block as was suggested by @TahirAhmadov should work for all cases I can produce, and would allow the reduction of Extension methods. However, the dichotomy in blocks is not desirable to our target audience. I believe they'll want the effect of being able to call any method or setter inside the same block, without differentiation. Although, this approach is a decent "alternative" that I haven't seen done before, which can grossly reduce the Extension method count.

I'll be back sometime soon with more examples to post.

jmarolf commented 2 years ago

@najak3d I would love to see some specific examples of where the language falls down. I will admit the cases are not clear to me today.

Looking at darts construction: https://github.com/flutter/samples/blob/ae50cc192e1c0902d05dbd8e9a813ed0615efdea/desktop_photo_search/fluent_ui/lib/main.dart#L62-L96

I don't see how it that different (or at least vastly superior to) Xamarin's markdown syntax: https://github.com/xamarin/XamarinCommunityToolkit/blob/f892c5fb7a562dbeb42bb724abc2ebaa780eca5e/samples/XCT.Sample/Pages/Markup/SearchPage.cs#L40-L74

Or the upcoming comet syntax for MAUI: https://github.com/dotnet/Comet/blob/87b0d575d62215b87725875a2f51ad6b6f9a4cbe/sample/Comet.Samples/Views/BasicTestView.cs#L42-L56

But I have not written a production app in all three frameworks so there are likely things I am missing. It may be best to have this as a separate discussion on https://github.com/dotnet/csharplang/discussions to start. if @Clancey or any folks from @dotnet/maui-internal-contributors want to chime in and talk about specific areas where the language has made things harder that would be extremely helpful.

As it stands today this discussion seems a bit orthogonal to @CyrusNajmabadi's proposal and I would prefer that we discuss potential problems of compound assignment in object initializers.

najak3d commented 2 years ago

The main "fall downs" is that if you need to do anything other than setting simple properties/fields, you cannot do it without writing EVERY possible thing you might want to do as an Extension Method to enable the very popular Fluent-Pattern.

Since Fluent-Pattern is so popular and accepted, IMO, it begs the question "why does it require so much extra code to enable this syntax", when it could be fully enabled if C# simply enabled you to call methods on the newly constructed class via ".{methodName}(...)" notation.

Xaml Markup, Comet, WPF -- all require considerable amount of "Extension methods" to achieve this very popular/widespread Fluent syntax. The required Fluent-Pattern-enabling-methods are obtuse in nature, and work like a hack (non-standard to how you generally write good C#), forcing you to write methods that must return the Object itself, so that the next command can then operate on it.

While the ".{methodName}(...)" notation requires ZERO odd-ball methods/extensions to achieve an intuitive, simple, and popular behavior, useful for initializing and instantiating your full objects inside of a composite/nested format. It still seems obvious to me that C# should allow this natively, without hack-ish extensions.

For example, to apply a "style to UI elements" you can call a "With(Action stylingAction)" on a newly created UIElement, and this method will apply Font Size/Style/Color, and Margins/padding/etc, and so requires a method call. And for other objects you might call "Enable()" or "Initialize()" right after constructions... which are also methods (not properties). And one of the biggest reasons to call methods is that WPF, Xaml, Avalonia, etc -- make heavy use of "AttachedProperties" which aren't native C# Properties, but instead are Read/written via Method calls -- so currently you can't set ANY AttachedProperties from inside a standard C# Init block.

And so, in order to achieve the most attractive syntax for Fluent-Pattern inside C#, requires an EVERYTHING approach, which means that even to set individual properties, you have to create a Fluent-style Method for EACH simple property as well, such "SetHeight(height)" which returns back the object.

But we note that the proposal on new Roles/Extensions, however, helps with this situation very much -- and so we're looking forward to that one being implemented, as it'll provide a better solution than the current solutions employed to enable Fluent Pattern.

CyrusNajmabadi commented 2 years ago

it begs the question "why does it require so much extra code to enable this syntax"

It doesn't require "so much extra code". Fluent can be done with just a dot and the method name. That is an extremely small amount of extra code.

The primary issue seemed to stem from the subjective distaste you have for that syntax here.

Note, your own syntactic proposals here either need the same amount of "extra code", or more. Your code would still need a dot and the method name, and you'd have to follow the call with a comma as well.

CyrusNajmabadi commented 2 years ago

It still seems obvious to me that C# should allow this natively, without hack-ish extensions.

Describing one is the most common and will understood language constructs that have been around more than 15 years as "hackish" is not an argument. You have subjectively chosen to not use the feature that was added where this sort of use case was one of the scenarios it was designed for.

Stop calling it hackish when all you really mean is "I don't like it, and I want something equivalent that I just personally think it's nicer".

najak3d commented 2 years ago

I cannot stop calling it "hackish" in the manner of what you have to do to "enable Fluent Pattern".. Once you write the abundant extensions to cover ALL things you want to set (including simple properties) - now you can write simple Fluent syntax. Fluent Pattern was a "hack" of the C# language to make it do something that you (and many others here) seem dead set against allowing C# to do natively without the hack. And since C# doesn't allow this natively, we all continue to do this hack, so that we can enjoy the syntax that so many have grown accustomed to.

I'll probably never ever ever see the harm, confusion, nor danger of enabling this type syntax: (because there truly is not confusion, danger, or harm here)

new Label()
{
Text = "This should be legal",   // simple property
Font = Fonts.Tahoma,  
.SetStyle(TextStyle, IsItalic),   // Init method, but have more than one parameter
.SetRowCol(2, 3),     // Sets a virtual Attached Property
.Bind(ModelProperty, Mode.TwoWay)   // multiple arguments
.MouseOver += _HandleMouseOverLabel    // something you are wanting to add here already
}

How could this code here ever be unclear or confusing, or ambiguous? It's ideal, and enables the Fluent-style initialization without ANY added extensions! (all of the methods called above are just methods that already exist)

CyrusNajmabadi commented 2 years ago

At this point @najak3d were at an impasse. I ask that you make another discussion as this is entirely off topic for how proposal and you are not addressing counter points and questions. Consistently coming back to a position that you state is unwilling to consider other prescribed perspectives means there's nothing more to discuss on this.

najak3d commented 2 years ago

Alright, let's just talk about your proposal here, to allow this notation:

var timer = new DispatcherTimer {
    Interval = TimeSpan.FromSeconds(1d),
    Tick += (_, _) => { /*actual work*/ },
};

This is ALREADY the equivalent of this in C#, using Fluent-Pattern. The steps to do it are widespread and popular -- it's called Fluent-Pattern. All you have to do is write 100's of Extension methods so that you can do what you want for initialization logic, including adding an extension that permits you to add event handlers.

The resulting Fluent-Pattern style logic simply looks like this:

var timer = new DispatcherTimer()
    .Interval(TimeSpan.FromSeconds(1d))
    .Tick((_, _) => { /*actual work*/ })

So there is ZERO need for your proposal. It's a useless proposal, that stems from YOU NOT LIKING FLUENT PATTERN. Why is it that you don't want to simply use Fluent Pattern for this? It's a wonderful thing, you say.

By your own arguments, you've FULLY de-legitimized your own proposal here.

You just wrote: "[Fluent-Pattern] one is the most common and will understood language constructs that have been around more than 15 years"

So why don't you want to solve your own issue with Fluent-Pattern??? It would work "wonderfully" for it.

najak3d commented 2 years ago

I just want to take this "wonderful Fluent Syntax" and wrap it in "{ }" so that you can precede your Fluent style method calls with simple property setters, AND requiring ZERO extension methods to make it work.

Fluent is such a wonderful/popular pattern, so why does C# require a hack to make it work? (a hack that even YOU don't want to do for your own issue)

theunrepentantgeek commented 2 years ago

Assume for the sake of argument that C# was modified so that all methods could be called within initializer syntax.

What effect would this have on the overwhelming majority of types that are not designed with a fluent interface?

In a few minutes of thought I came up with several nasty scenarious - not just code that's difficult to read, but code that doesn't do what it looks like it does, and code that would fail at runtime.

Language design must address more than "which useful scenarios does this enable" - it also needs to cover "which nasty outcomes does this enable" too.

CyrusNajmabadi commented 2 years ago

Why is it that you don't want to simply use Fluent Pattern for this?

I never said I didn't. Stop strawmanning. I was explicit that I thought that absolutely should be part of the discussion.

So why don't you want to solve your own issue with Fluent-Pattern??? It would work "wonderfully" for it.

That is absolutely part of the discussion and something we will consider.

Is this supposed to be some sort of 'gotcha'? We do absolutely consider these things and often will reject proposals precisely because of this sort of thing.

najak3d commented 2 years ago

@theunrepentantgeek - C# enables plenty of nasty looking code, if a bad developer decides to create a cryptic mess. Unsafe code can look nasty - yet we allow it. Extensions can literally make ANYTHING possible - yet we allow it.

I would only be concerned if something enabled by C# introduced a "new confusion" where you could think you are doing one thing, but then something else happens, that is hard to figure out. It's one reason C# broke the awful mistakes of C++, such as with memory leaks, and also with statements like: "If (a = b) {}" ... where the coder meant to use "==".

C# still allows unsafe code, which opens doors for memory leaks, but we allow it.

So I'm interested to see your "reasonable/common examples" that you perceive to be the risk for simply allowing the ".method()" notation to be called on a newly constructed object, behaving the exact same as if you had done long-hand coding of the same thing. (where you assign a local variable, then call all those methods using the local variable reference for each call)

I don't see how you would ordinarily do anything "confusing" producing unexpected results for ordinary looking code. Please provide your best 3 examples here, so that we can know what you are talking about.

najak3d commented 2 years ago

Also, what do you mean by types "not designed with a fluent interface"?? Every class has a "fluent interface" if you simply write the extensions. No good class SHOULD EVER have a native Fluent-Interface, because the fluent-pattern-methods are nonsensical in nature -- thus, as is, since it's a hack -- fluent-interface methods should ALWAYS be added via Extensions, or by creating a dedicated "Builder class" that provides the fluent interface for the composition context.

OR -- C# could simply allow it, because it's very sensible and non-confusing/non-dangerous.

CyrusNajmabadi commented 2 years ago

because the fluent-pattern-methods are nonsensical in nature

this position is going to go nowhere. You are effectively stating an unwillingness ot accept or use a bog-standard pattern that .net and other APIs have had for 15+ years. Any argument that comes down to your dislike of this aspect of our ecosystem isn't helping as it effectively is you stating htat we need to be constrained by your own personal foibles.

CyrusNajmabadi commented 2 years ago

@najak3d I left you some info on Discord :)

najak3d commented 2 years ago

Why is it that you don't want to simply use Fluent Pattern for this?

I never said I didn't. Stop strawmanning. I was explicit that I thought that absolutely should be part of the discussion.

I'm not strawmanning here. You were blatant in your response to me that my proposal has no ground to stand on, because it could be resolved using 100's of Fluent-extensions, to enable the simple syntax. You've cast me as someone who is petty and only proposing a change because I don't like the "current widely adopted/understood Fluent syntax". Yet, you are EXACTLY THE SAME with regard to your own proposal. Your proposal could be resolved by Fluent Pattern - so why raise this issue at all?

So you've asked me to "defend my position" -- why do I need to? You ALREADY NEED TO DEFEND MY POSITION, to promote this proposal here.

I KNOW why Fluent would suck as a solution for yours -- because it's an awkward Hack -- which is only used because it's better than not using Fluent method. And so we've adopted and standardized this Hack -- just because it's widely adopted, doesn't mean it's any less of a hack.

I'd like C# to support Fluent style initialization in a natural easy Hackless fashion, as it should. There's just not a strong defense for NOT simply enabling the ".method()" notation inside an init block.... at least not that I've ever seen.

najak3d commented 2 years ago

because the fluent-pattern-methods are nonsensical in nature

this position is going to go nowhere. You are effectively stating an unwillingness ot accept or use a bog-standard pattern that .net and other APIs have had for 15+ years. Any argument that comes down to your dislike of this aspect of our ecosystem isn't helping as it effectively is you stating htat we need to be constrained by your own personal foibles.

YOU ARE DOING THE SAME THING. That's my point. YOU ARE UNWILLING TO ACCEPT FLUENT AS A SOLUTION TO YOUR OWN PROPOSAL. Am I wrong here? You and I are in the same-boat, but you are pretending that I'm being "petty" while you are not.... but we're in the same boat here.

CyrusNajmabadi commented 2 years ago

Yet, you are EXACTLY THE SAME with regard to your own proposal. Your proposal could be resolved by Fluent Pattern - so why raise this issue at all?

So that can be discussed. It will be part of the convo about why we would need this if it's already possible. Indeed, tha'ts part of my discussion. I do not view extensions as a hack, so they will be fairly compared against the alternatives and the proposal will hve to stand against that. I do not dismiss existing language options as being unsuitable as 'hacks'.

YOU ARE UNWILLING TO ACCEPT FLUENT AS A SOLUTION TO YOUR OWN PROPOSAL

You are incorrect. I absolutely accept that as a solution. Indeed, i'll put that in the potential alternates section of the proposal right now.

--

Alternative has been added to the proposal. I do mention a limitation of it, namely that it will not work for init-only members, which we would want for things like records+with.

najak3d commented 2 years ago

You made this proposal because you think "It's likely a good idea". How did you ever come to this conclusion, if you so firmly believe that "Fluent is awesome" and so "let's just write 100's of extensions for each scenario where we want to enable event handling inside the init block"???? Why on earth did you even make your "absurd proposal" given your firm beliefs that Fluent is so awesome/accepted/appropriate?

The reason you don't want to use Fluent is that Fluent is annoying to implement, because it's essentially "all or nothing". You can't just write ONE METHOD to enable event handling, and be done. You pretty much have to wrap ALL of the possible properties/methods, else your fluent-options are incomplete.

CyrusNajmabadi commented 2 years ago

You made this proposal because

I made this proposal to bring as a potential option to the LDM to see if the feature is worthwhile and carries enough value for us to support.

You made this proposal because you think "It's likely a good idea". How did you ever come to this conclusion, if you so firmly believe that "Fluent is awesome"and so "let's just write 100's of extensions for each scenario where we want to enable event handling inside the init block"

I'm not going to engage if you're not acting in good faith. You quoted three things there that i can find no record of ever saying myself. This is a strawman. You're expecting me to defend myself against things i didn't say.

The reason you don't want to use Fluent is that Fluent is annoying to implement, because it's essentially "all or nothing". You can't just write ONE METHOD to enable event handling, and be done. You pretty much have to wrap ALL of the possible properties/methods, else your fluent-options are incomplete.

this is not true. I don't believe i have to wrap all the possible properties/methods. For example, i can just pass hte instance along and do whatever i want with it. One issue though is that this is not compatible with init properties.

najak3d commented 2 years ago

Suggesting Fluent as a viable alternative to resolve your issue, is a silly notion, and is a practice that shouldn't be made "more widespread".

I picture the fluent hack to be like this: Imagine a world where you aren't allowed to fly with ANY medications at all... No medications allowed on a plane -- either carry on or luggage!.... so if you want to bring medicine with you, you must drive, and it takes 10x longer, and more cost/risk. But it's YOUR ONLY OPTION, other than not going. So MILLIONS of people need medication and so choose to DRIVE, because it's their only option. But driving is better than NOT GOING... so they drive.

Then along comes someone, named kajan4D, who says "can't we just allow meds to be package into the luggage along with cosmetics"? And the airline says "why are you rejecting the widely accepted practice of driving??? It's widespread, and understood for 15 years, so it must be good; in fact, let's consider using this driving solution for MORE things."

In my view, that's what is happening here now. I'm being told to "keep driving because it works, and is widely practiced, and therefore there is nothing wrong with it".

Nobody should seriously consider Fluent as an acceptable hack to enabling Event handling. Nor should it be used for calling a plethora of other initialization methods.

TahirAhmadov commented 2 years ago

I will repeat my earlier suggestion again. Given that methods can't really be called before all properties are set, because of init/required scenarios, even if they were allowed to be called, they would have to be called at the end of the init-block. And instead of adding method invocations to init-blocks, why not just do this:

// one "fluent" extension method to catch all
public static T Configure(this T This, Action<T> action) { action(This); return This; }
var btn = new Button
{
  Text = "asd",
  Click += this.btn_Click,
}
  .Configure(b =>
  {
    b.WithStyle(style); // call existing methods which return void or whatever
    b.Enable();
    // etc.
  });
CyrusNajmabadi commented 2 years ago

Suggesting Fluent as a viable alternative to resolve your issue, is a silly notion,

It is not, and i really need to make it clear that you need to check your perspective on this stuff. It is absolutely appropriate and reasonable for people to suggest alternatives and to consider if "the status quo" is just more preferable than doing anything here.

We've survived without this for 20 years. We will absolutely be considering if we can keep on going without anything else here. And we will consider if hte existing solutions are viable.

I strongly suggest we chat. You keep presuming that we should view this space just like you do. And that sort of attitude is not going to go anywhere in terms of gaining consensus. I can tell you from 20 years doing this, it is not a good idea to tell people that their position is silly, as opposed to spending time and effort to understand how htey think about things and to work with them to find solutions that satisfy their concerns as well.

najak3d commented 2 years ago

It's unreasonable to believe that you were inspired to write a proposal to change C# that you didn't already think was "likely a good idea", despite seeing "how awesome Fluent already is". Why even bother with this proposal if you think Fluent is so great.

I get that you are open-minded and not drawing your final conclusions yet. But -- the fact remains is that you already knew about Fluent as a solution -- but STILL made this proposal, because you clearly perceived that your proposal was "Likely better" than using Fluent.

Yet when it comes to my proposal, you seem to instead champion Fluent, and mildly insult me implying that "Fluent just doesn't match my taste", without realizing that I don't like the "100's of hackish extensions required to enable the syntax." It's 100's of hackish methods that shouldn't ever NEED to exist in the first place, if C# simply allowed this very clear/sensible/non-dangerous syntax in the first place.

CyrusNajmabadi commented 2 years ago

Nobody should seriously consider

@najak3d this is not acceptable. Stop telling people how they should assess as situation. At the very least recognize that there is no viable path forward that involves browbeating people to assess things like you do. Please come to discord so we can chat about this.

CyrusNajmabadi commented 2 years ago

Why even bother with this proposal if you think Fluent is so great.

To have the situation be discussed and evaluated. Because i strongly want to get the views of the rest of hte ecosystem, as well as the rest of the LDM to see how they feel about this.

I have come with many proposals over the years that i've had varying levels of supportive thoughts around. I've even brought things that i did not think was a good idea, just so it could be assessed by others to get their views. Please stop presuming that this process works how you want it work and that others need to feel the same way about these topics as you do.

The reason we have this process and the reason why we have so much discussion and communication here is because there are so many different perspectives and so many different views on what is or isn't valuable for the language.

CyrusNajmabadi commented 2 years ago

without realizing that I don't like the "100's of hackish extensions required to enable the syntax."

That was a decision on your part. No one here ever suggested or implied you should do that.

najak3d commented 2 years ago

I will repeat my earlier suggestion again. Given that methods can't really be called before all properties are set, because of init/required scenarios, even if they were allowed to be called, they would have to be called at the end of the init-block. And instead of adding method invocations to init-blocks, why not just do this:

// one "fluent" extension method to catch all
public static T Configure(this T This, Action<T> action) { action(This); return This; }
var btn = new Button
{
  Text = "asd",
  Click += this.btn_Click,
}
  .Configure(b =>
  {
    b.WithStyle(style); // call existing methods which return void or whatever
    b.Enable();
    // etc.
  });

Why not simplify that syntax to simply this?

> var btn = new Button
> {
>   Text = "asd",
>   Click += this.btn_Click,
>    .WithStyle(style), // call existing methods which return void or whatever
>    .Enable()
>   };

It's considerably less typing and less tedious -- without introducing ANYTHING confusing or ambiguous? That makes it "easier to understand" because it communicates the exact same thing, with fewer words.

CyrusNajmabadi commented 2 years ago

without introducing ANYTHING confusing or ambiguous?

Nothing is confusing or ambiguous about that solution for me. It's literally some of hte most vanilla C# that has been supported and clear for 17 years now.

because it communicates the exact same thing, with fewer words.

Brevity can be valuable. But it's not necessarily something we work overly hard on if we feel the existing solution is sufficient. Please see the link i mentioned in discord (about -100 points). The code provided is idiomatic and universally supported everywhere for anyone using the language post 2005. So we strongly consider if we should stick with the status quo if it's sufficient and if the new feature isn't much better.

Furthermore, as i mentioned already in the first response:

Yes. Methods imply state mutation as opposed to declarative construction. So I feel that it's much more natural for them to be after the instance read constructed.

najak3d commented 2 years ago

without realizing that I don't like the "100's of hackish extensions required to enable the syntax."

That was a decision on your part. No one here ever suggested or implied you should do that.

It just seems like you are wishy-washy on your stance. Wasn't it you that said "Fluent-Pattern is widespread for 15 years"... I can either choose to have nice Fluent-Syntax for our users, or not. So we choose to suffer-the-pain, to give our users a better experience. But there's just no good reason that "Fluent-Pattern should require this added Pain, just to enable it" -- not when there is a very simple/viable/sensible solution that would make this pain go away 100%, without introducing any new viable dangers (that I have ever seen).

TahirAhmadov commented 2 years ago

Really though, I'm not sold on the idea that one needs to call all that many methods to construct UI controls. I've been doing WinForms and ASP.NET WebForms for years and WPF for a year or two, and I never had the need to call methods on the controls to initialize them. Even when I created custom WinForms controls, I added properties for which the designer auto-generated init code without issues, using the appropriate attributes and converters and such. Same thing with ASP.NET ASPX markup language and WPF XAML - both of those markup languages work fine because all you need 99.99% of the time is to set properties.

Why not simplify that syntax to simply this?

I guess the question is, is it worth the effort to add this, if all you are saving is literally 3 lines - the .Configure(b=>, { and }). I understand and agree that 3 lines can get multiplied by many times, and that goes back to what I wrote above - I would like to see concrete examples of things that need to be methods. You brought up a couple more:

For example, to apply a "style to UI elements" you can call a "With(Action stylingAction)" on a newly created UIElement, and this method will apply Font Size/Style/Color, and Margins/padding/etc, and so requires a method call. And for other objects you might call "Enable()" or "Initialize()" right after constructions... which are also methods (not properties). And one of the biggest reasons to call methods is that WPF, Xaml, Avalonia, etc -- make heavy use of "AttachedProperties" which aren't native C# Properties, but instead are Read/written via Method calls -- so currently you can't set ANY AttachedProperties from inside a standard C# Init block.

Let's go through these one by one. The With(Action stylingAction) is an interesting approach, however, if it was me, I would have a strongly typed interface, IStyleAdder or something, and then the control can have a property of that type; but you can even create a property of type Action if you like. Enable() is begging to be changed to bool Enabled { get; set; }. I don't understand what Initialize() means - what is it supposed to do? Regarding attached properties, those are a bit more finicky; personally, I dislike that whole approach - after all these years it's still weird to me; however, my opinion of attached properties aside, if you need to set those, check out the discussion Fred (I think) started about adding attached properties as part of the roles feature.

CyrusNajmabadi commented 2 years ago

So we choose to suffer-the-pain

I would suggest coming to discord. On a personal level, i will tell you that at this point is appears to me that you're not interested in even trying to reach any sort of consensus or to get someone like me to champion your position. That you can only categorize our views as causing people ot "suffer the pain", or insist on strawmen like claiming that we're telling people to write hundreds to thousands of extension, or stating that i'm not willing to consider alternatives like fluent myself (where i am), means that you're not really interesting in engaging.

I have been trying to explain my positions and explain the concerns i have with yours. It does not feel like you are willing to accept that other people may feel differently than you on these topics, and that means there is very likely no paths forward here.

If you want to come to discord, we can discuss further how best to potentially get interest (and a champion) for your proposal.

najak3d commented 2 years ago

@CyrusNajmabadi wrote: "Yes. Methods imply state mutation as opposed to declarative construction. So I feel that it's much more natural for them to be after the instance read constructed."

Which makes for a simple rule that would cause a compiler error -- it treats the init block the same as it does now, UNLESS it comes across a ".method(..)" -- at which point the object becomes "fully constructed" and the next lines of code become the exact equivalent to long-hand calling of methods AFTER the init-block. Putting inside the init-block is just for short-hand... because it's still 100% clear what you are trying to do.

AND it enables us to support Fluent-Pattern without any hacks. It would then "just work".

CyrusNajmabadi commented 2 years ago

Putting inside the init-block is just for short-hand...

I get that. I'm saying, i find extremely marginal benefit for such a shorthand. I'm not a believer in the idea that this is common/widespread. Furthermore, your claims that this causes thousands of extensions to be written just seems off to me as it hasn't been explained why you'd need more than a single extension here

public static T Init<T>(this T value, Action<T> init)
{
    init(value);
    return value;
}

Why do you need thousands?

It would then "just work".

From my perspective, it works fine today.

najak3d commented 2 years ago

@CyrusNajmabadi wrote: "From my perspective, it works fine today."

Which seems to also mean, "from YOUR perspective, adding event handlers via Fluent Pattern ALSO works fine today"-- because it does -- so long as you don't mind converting everything over to Fluent pattern, or tacking them on AFTER the init block. Both of those "work fine today" as well.

I've never heard of someone saying "I can't add event Handlers after construction" -- there's no issue here. They obviously can do this already in a variety of ways. "It already works fine today." ... So why did you make a proposal to fix something that already "works fine today"????

Fluent does work fine; but it's still extra work, and a hack. We'll continue to use it, unless C# ever matures to a point where we don't need this hack to have this simpler syntax.

The new Roles/Extensions does offer a solution that is superior to current Extensions, but still isn't as good as simply allowing this syntax to exist inside the init block.

HaloFour commented 2 years ago

@najak3d

This is the kind of friction that you get when you take one API and try to force it to follow the conventions of a completely different API. Stop doing that and those "hacks" disappear. If you want MAUI or .NET UI APIs to follow a more fluent API design, maybe take that up with them. This is not a language concern, and aside from event handlers in initializers I've yet to see anything in Flutter that doesn't fit in the existing design of C#.

jmarolf commented 2 years ago

wew, guess I'll wade in here again

Object construction is one of the most common things done in C# today (if not the most common) so improving it would certainly be worthwhile. However, I would like us all to be very specific about the problems we see today with object construction before jumping to any conclusions about what the design to fix them should be.

From my perspective we have two kinds of objects that are commonly constructed today.

For plain-old-data the records feature was added with the hope that it would make their construction simpler. I would say this has mostly been a sucess though there are areas for improvement. Most importantly to me POD is very often serialized and in the world we live in today that often means setting properties.

In the world of POD that means constructors are not very useful so if I declare a record like this:

public record WeatherForecast(DateTimeOffset Date, int TemperatureCelsius, string? Summary = null);

It's not likely that is constructor ever gets called in real code because it most likely always going to be initialized like this:

WeatherForecast? weatherForecast = JsonSerializer.Deserialize<WeatherForecast>(jsonString);

which leads us to the place we have today where most POD type are written like this:

public record WeatherForecast
{
    public DateTimeOffset Date { get; init; }
    public int TemperatureCelsius { get; init; }
    public string? Summary { get; init; }
}

The fact that PODs need to be serializable and often represented in the system as json, xml, etc leads to proposals like Dictionary Literals and of course the extremely exciting Roles and extensions.

However, those same requirements also mean that fluent construction of these types must not be required because asking a serialization framework to call your fluent apis is too much complexity to put on them imho.

var forecast1 = new WeatherForecast{
    Date = DateTimeOffset.UtcNow,
    TemperatureCelsius = 10,
    Summary = "Redmond Forecast"
};

var forecast2 = forecast1 with {
    TemperatureCelsius = 15,
    Summary = "Kirkland Forecast"
};

var forecast3 = forecast2
    .WithTemperatureCelsius(12) // how is a serialization framework going to know to call these?
    .WithSummary("Bellevue Forecast");

My general opinion is that records and with-expressions are supposed to primarily apply to POD-like scenarios and so I am not concerned about fluent api syntax there. My expectation is that Required Properties and other proposals that focus on improving simple object construction will help more people in for now.

All my concerns about POD and fluent apis don't apply to "rich" objects though (note I am only using quotes here because rich can mean so many things). UI is a good example of rich objects imho. They encapsulate state and need to manage much more than plain-old-data. For construction of these types of objects we saw the fluent API pattern develop in C# 17 years ago.

But we don't just use fluent apis in UI frameworks. We also use a fluent builder pattern today in the default ASP.NET template:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()){
    app.UseExceptionHandler("/Error");
}
else{
    app.UseDeveloperExceptionPage();
}

app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

but I am not totally convinced that allowing method calls here would make things look much cleaner or be easier for developers to use and understand:

var app = WebApplication.CreateBuilder(args){
        .Services.AddRazorPages()
    }.Build(){
        .Environment.IsDevelopment()
            ? .UseDeveloperExceptionPage()
            : .UseExceptionHandler("/Error"),
        .UseStaticFiles(),
        .UseRouting(),
        .UseAuthorization()
        .MapRazorPages(),
    };

app.Run();

or if we allowed "withers" to work here

var builder = builder with { .Services.AddRazorPages() };

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()){
    app = app with { .UseExceptionHandler("/Error") };
}
else{
    app = app with { .UseDeveloperExceptionPage() };
}

app = app with {
    .UseStaticFiles(),
    .UseRouting(),
    .UseAuthorization(),
    .MapRazorPages()
};

app.Run();

To be clear this may not be the best example, but I think it's relevant. Whatever we do should make sense for all the various domains people use the language.

It sounds like what you want @najak3d (though it's always dangerous to assume here...) is a pipe-forward operator.

Here is an example in F# is pipe-forwards for their web template (though this is not apples to apples as the F# template has more features)

let todosApi =
    { getTodos = fun () -> async { return storage.GetTodos() }
      addTodo =
          fun todo ->
              async {
                  match storage.AddTodo todo with
                  | Ok () -> return todo
                  | Error e -> return failwith e
              } }

let webApp =
    Remoting.createApi ()
    |> Remoting.withRouteBuilder Route.builder
    |> Remoting.fromValue todosApi
    |> Remoting.buildHttpHandler

let app =
    application {
        url "http://0.0.0.0:8085"
        use_router webApp
        memory_cache
        use_static "public"
        use_gzip
    }

run app

with me pulling a pipe-operator syntax for C# from thin air, things could look something like this:

var app = 
    args |> WebApplication.CreateBuilder
    builder |> AddRazorPages;
    builder |> Build;
    app |> app.Environment.IsDevelopment()
        ? UseDeveloperExceptionPage
        : UseExceptionHandler("/Error");
    app |> UseStaticFiles;
    app |> UseRouting;
    app |> UseAuthorization;
    app |> MapRazorPages;

app.Run();

There are lots of reasons that a pipe-forward operator would take a long time to get right, but if you think this would be helpful, I invite you do leave your thoughts on this discussion.

jmarolf commented 2 years ago

As for @CyrusNajmabadi's actual proposal, I am curious what the LDM thinks. I don't think this is a bad addition, but I do not think there is overwhelming user pain today.

CyrusNajmabadi commented 2 years ago

It sounds like what you want @najak3d (though it's always dangerous to assume here...) is a https://github.com/dotnet/csharplang/discussions/74.

Alternatively, i think that auto-fluent for void methods could work out for him. That's something i'm far more likely to champion.

jmarolf commented 2 years ago

ah yes, that would be significantly less design work. and certainly would make things better.

jmarolf commented 2 years ago

I don't know what the scenario is where I will think that we must have a forward-pipe operator. Maybe someday some important pattern (model-view-update?) will be so much better that we will be made to do it, but I am not seeing that happening in the short term.

najak3d commented 2 years ago

It sounds like what you want @najak3d (though it's always dangerous to assume here...) is a #74.

Alternatively, i think that auto-fluent for void methods could work out for him. That's something i'm far more likely to champion.

IMO, "fluent-everywhere" is far more likely to cause mistakes/confusion than what I'm asking for. In "fluent-everywhere" so you used to have a function that returned some other object, and so the code that uses it was expecting an entirely DIFFERENT object to be returned, and was operating on it... Now you change the return type to "void" -- and there are NO COMPILER ERRORS -- the calling code now auto-operates upon the "void" which is interpreted to be the hosting object... Oops. IMO, that is likely a very bad idea.

najak3d commented 2 years ago

I don't know what the scenario is where I will think that we must have a forward-pipe operator. Maybe someday some important pattern (model-view-update?) will be so much better that we will be made to do it, but I am not seeing that happening in the short term.

Thank you for your thoughtful response above. Even for "rich" objects Serialization is heavily used in many settings. Construction via serialization is everywhere.

What I'm wanting is simple, clear, and non-dangerous in nature. It ONLY applies to the "Init Block" and simply makes the Init-Block work like a "Wither" block, with one caveat that the method calls must be at the end of the block, following the property setters. So when compiler sees the first ".method()" statement, it completes construction, and the remainder of the methods calls are now operating on a constructed object. It would be equivalent, exactly, to the long-hand notation. It's simple, scoped short-hand, to aid in complex code-based compositions.

My request isn't truly "Fluent" -- it's a simple attractive replacement for Fluent, with notation that looks about the same, but does NOT require methods to return back the object instance to the caller.

This does NOT interfere ANY with Serialization concerns. In fact, in some cases, it'll enhance Custom Serialization logic. Imagine a custom serialization method like this:

MyType Deserialize(Reader reader)
{
   MyType obj = new MyType()
  {
            Name = reader.ReadString(),
            .ApplyStyleByName(reader.ReadString()),
            .SetPosition(reader.ReadInt32(), reader.ReadInt32()),
            .SetMargins(reader.ReadString()),  // string representation of the Margins - this would be an extension method
            FontSize = 12  //>> COMPILER ERROR.. Property settings must precede method calls
  };
}

Serialization could simply choose whether or not they prefer this short-hand or not. If not, then it makes zero impact on concerns for Serialization and Records, or PODs.

najak3d commented 2 years ago

I created a new-clean proposal here, that presents what I'm aiming for more clearly from the start.

https://github.com/dotnet/csharplang/issues/5727

My new proposal fully accommodates/solves this proposal's objectives, and then some.

najak3d commented 2 years ago

Regarding serialization -- there is one mode of serialization that I've seen, which is high-performance, and easy to debug, and maintain backward compatibility. Instead of saving data, it saves C# code, which is then compiled, and has the code that directly instantiates an object.

So just imagine a serializer that generates the C# that initializes each object, with the values directly inserted into the C#. Then compile that, and load it as a dynamic DLL, and simply run it.

Backward compatibility becomes not too hard, because you approach it simply as you would approach backward compatibility for your API... if your API is backward compatible, then the old serialized DLL's will also work for the new API.

Other great thing about it is transparency and ease of debugging -- you aren't dealing with binary/string data... but you can simply look at it like raw code. And even see compiler errors, where backward compatibility might be broken.

In ways, it's the best of many worlds. I've used this approach for complex 3D scenes in the past. Having nice C# syntax for construction would be useful for this type of serialization, especially.

This mode of serialization is how WinForms works as well.

HaloFour commented 2 years ago

@najak3d

So just imagine a serializer that generates the C# that initializes each object,

This would be a library concern, not a language concern.

jmarolf commented 2 years ago

Instead of saving data, it saves C# code, which is then compiled, and has the code that directly instantiates an object

You have just re-invented binary serialization from first principles. While this design is attractive in its simplicity of use it has several irreconcilable security flaws that have caused many real-world security exploits.

Security aside, what if you want to inter-operate with non-C# code? Being able to easily host a REST api that returns json is one of the cornerstones of why people use ASP.NET or microservices in general. The default design in C# cannot be "other forms of serialization are second class" when they represent 90% of what people are using serialization for.

najak3d commented 2 years ago

@najak3d

So just imagine a serializer that generates the C# that initializes each object,

This would be a library concern, not a language concern.

What that generated C# looks like is a language concern. Is it succinct, or verbose? Does it require extensions to make it work, or does C# syntax allow for the simpler syntax naturally?