csharplang icon indicating copy to clipboard operation
csharplang copied to clipboard

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

Open CyrusNajmabadi opened this issue 2 years ago • 112 comments

Compound assignment in object initializer and with expression

  • [x] Proposed
  • [ ] Prototype: Not Started
  • [ ] Implementation: Not Started
  • [ ] Specification: Not Started

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

CyrusNajmabadi avatar Sep 10 '21 18:09 CyrusNajmabadi

I would use this feature. I want to know how it would handle property changes that trigger an event. What if something like this

var myText= new MyText{
    Text = "Hello"
}
myText.PropertyChanged += delegate {};

got refactored to

var myText = new MyText{
    PropertyChanged += delegate {},
    Text = "Hello"
}

before the event would not trigger, and after it would get triggered. Does the order matter? Would swapping them make the event not trigger?

var myText = new MyText{
    Text = "Hello",
    PropertyChanged += delegate {}
}

Should the language enforce the events get added last, or should auto refactoring tools try to add the events last, to avoid triggering them on accident?

wrexbe avatar Sep 11 '21 20:09 wrexbe

That's an existing question that applies to assignments of properties. I would be very confused if the syntax did not desugar to a series of statements in the order that the members are initialized in syntax.

What if someone wants to rely on PropertyChanged getting called when Text is set, and blocking them doing it in their preferred order is just annoying them?

jnm2 avatar Sep 11 '21 20:09 jnm2

Yeah that makes sense. Just worried about someone moving them around to be ABC order. I would probably keep the event initializers at the bottom most of the time.

wrexbe avatar Sep 11 '21 20:09 wrexbe

Should we allow this for with { ... } expressions? I'm not sure.

While my scenario isn't about events, I recently encountered a case where the lack of compound assignment in with expressions sent me down an "expression-body-to-statement avalanche". The case was updating some deep record structure to increment a TotalRuntime accounting property of type TimeSpan with the Elapsed time obtained from some Stopwatch:

var newStats = oldStats with { /* update a bunch of properties here ... */ Elapsed += sw.Elapsed, /* ... and more here */ };

bartdesmet avatar Sep 14 '21 05:09 bartdesmet

Thanks @bartdesmet I'll definitely bring up the question if we want to support this in with as well as supporting compound assignment for more than just events.

CyrusNajmabadi avatar Sep 14 '21 05:09 CyrusNajmabadi

While my scenario isn't about events, I recently encountered a case where the lack of compound assignment in with expressions sent me down an "expression-body-to-statement avalanche".

This has happened to me as well.

jnm2 avatar Sep 14 '21 16:09 jnm2

For me especially operator ??= would be very useful in with expressions.

bernd5 avatar Sep 14 '21 20:09 bernd5

This would be very useful when constructing a "tree" of objects, like UI controls, but also anything else, where children are added inline:

this.Controls.Add(new Panel
{
  Controls = new List<Control>
  {
    new Button
    {
      Text = "Submit",
      Click += this.btnSubmit_Click,
    },
    new Button
    {
      Text = "Cancel",
      Click += this.btnCancel_Click,
    },
  }
});

TahirAhmadov avatar Oct 25 '21 13:10 TahirAhmadov

Question: the same field/property cannot be assigned twice, but should it be possible to attach 2+ handlers to the same event? Would it be by repeating the event name, or separating the handlers with commas or something like that?

var btn = new Button
{
  Click += this.btn_Click,
  Click += this.anyControl_Click,
// or
  Click += [this.btn_Click, this.anyControl_Click],
};

TahirAhmadov avatar Nov 03 '21 20:11 TahirAhmadov

Click += new EventHandler(this.btn_Click) + new EventHandler(this.anyControl_Click) would probably fall out automatically.

jnm2 avatar Nov 03 '21 20:11 jnm2

Is there a good reason to not simply allow you to call ANY member method of the newly created object (and treat it the same as if you called it after construction)?? After all, property setters are really just calling the "Property_Set(value)" method; so why not simply extend this capability to allow calling ANY class method for the instance being constructed?

So for the event handler registration example, the answer is clear -- just have two entries:

var btn = new Button()
{
    Clicked += EventHandler1,
    Clicked += EventHandler2,
    AnyClassMethod(args),
    AnyExtensionMethod(args)   
}

It simply means that every statement inside the brackets ({ }) is treated as though it were prefixed by "btn.".

This is the Simplest solution/rule, and offers the maximum benefit.

=== NOTE: This also helps those who don't want to be forced to make all Property Setters Public. For example, I prefer, for many instances, to only make Get() public, and for the Set, create a different method with "reason" tag:

public int MyProp { get; private set; } // Setter is PRIVATE

public void SetMyProp(int val, string reason)  // USE THIS METHOD TO SET the property
{
     // Here I can enable Logging, which will now include the "reason" without doing expensive reflection techniques.
}

This makes it very easy to find out "why the value was set" later on, simply by looking at Log output, and also makes it easier to set Breakpoints for specific "reason" values.

Using this technique, currently disables our ability to use the Object initializer, because my properties don't have public setters.

What I'd prefer to do is:

new MyClass()
{
   SetMyProp(value, "Construct")
}

Currently, we cannot use Object Initializers for this mode of API.

IMO, simply allow Object Initializer blocks to call ANY method on the instance, not just Property Setters.

najak3d avatar Jan 16 '22 00:01 najak3d

Is there a good reason to not simply allow you to call ANY member method of the newly created object

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.

Also, that syntax is already allowed and is used for collection initialization. So it would need to be a syntax that would not be ambiguous with that.

CyrusNajmabadi avatar Jan 16 '22 01:01 CyrusNajmabadi

Methods imply state mutation as opposed to declarative construction.

This doesn't make sense to me. I agree that methods imply state mutation, but state mutaion is NOT opposed to declarative construction. Construction is really a kind of mutation in C#, because C# currently doesn't have a good mechanism to distinguish the two, until we have required init property and init-only methods. Also, although event registration should be seen as construction, a more general compound assignment is more like a state mutation. In any case, you shouldn't say something like "we don't want to add this because it is not initialization/construction".

acaly avatar Jan 16 '22 02:01 acaly

Construction is really a kind of mutation in C#, because C# currently doesn't have a good mechanism to distinguish the two

I disagree. I think we do, and part of that is not blurring the lines more by using methods in these scenarios.

CyrusNajmabadi avatar Jan 16 '22 03:01 CyrusNajmabadi

The long-term trend has been that programmers want simpler (more terse) syntax. Less typing accomplishes more (and thus less reading too).

For UI construction, some controls are written where the Children list is "Get-only" -- you can modify the contents of the list, but not "set the list" -- e.g. you can call AddChildren, or AddChild, but not "Children = new List<T>()".

So UI composition that SHOULD work without writing a full suite of hackish extension methods is the following:

new Grid()
{
   Prop1 = propVal,
   Prop2 = propVal2,
   AddChildren(
      new Button()
     {
        Clicked += EventHandler,
        BindTo(binding),
        ///... more 
    },
    new Button()
    {
       // compose Button2 here
    }
   )  // end of AddChildren(..)
} 

If a developer thinks "methods don't belong in initialization", then that programmer can decide to not-call-methods in the intializer. But for the context where it is deemed grossly useful (e.g. UI composition) - allow it. Why not?

najak3d avatar Jan 16 '22 05:01 najak3d

Why not?

Wrong question. Language features are tremendously expensive. The question needs to be Why?

And it needs a compelling answer, far more than Why not?

theunrepentantgeek avatar Jan 16 '22 05:01 theunrepentantgeek

The long-term trend has been that programmers want simpler (more terse) syntax.

This is not the long term trend at all. And you can see very popular and very verbose languages that show that.

Less typing accomplishes more (and thus less reading too).

This is not a pro. Clarity is what matters, not terseness.

For UI construction, some controls are written where the Children list is "Get-only" -- you can modify the contents of the list, but not "set the list" -- e.g. you can call AddChildren, or AddChild, but not "Children = new List()".

This already works in C# today. Just do this:

new Whatever
{
    Children = { ... }
}

This will add to the children list. No need to new it up.

If a developer thinks "methods don't belong in initialization", then that programmer can decide to not-call-methods in the intializer.

Or we can just not allow it at all if it's not going to be a good thing :)

--

note @najak3d please use normal github markdown markers around your code. e.g. ```c#. I've edited your posts to use them properly :)

CyrusNajmabadi avatar Jan 16 '22 05:01 CyrusNajmabadi

So UI composition that SHOULD work without writing a full suite of hackish extension methods is the following:

You can already do this in C# today with:

new Grid()
{
   Prop1 = propVal,
   Prop2 = propVal2,
   Children =
   {
      new Button()
      {
          Clicked += EventHandler, // will be supported by this proposal.
      },
      new Button()
      {
         // compose Button2 here
      }
   }  // end of Children
} 

CyrusNajmabadi avatar Jan 16 '22 05:01 CyrusNajmabadi

Less typing accomplishes more (and thus less reading too).

The two aren't strongly correlated. Shorter means more opportunity for confusion (does the code you're reading do what you think it does).

It's so easy to go past concise to cryptic, particularly when you're familiar with something and your readers may not be.

Real world example:

What does SGTM mean?

Sounds Good To Me or Silently Giggling To Myself?

Assuming the wrong one once caused me some minor embarrassment.

theunrepentantgeek avatar Jan 16 '22 05:01 theunrepentantgeek

I am very impressed with the community here. This was my first day visiting these forums. I'm a 51 year old programmer of C# since 2003, and want to see this language "rule all" -- but it appears to me that C# has lost ground in the last 10 years, mainly due to Microsoft doing a piss-poor job of making Xamarin Forms competitive/good (they work worse than WPF). Thus we have competitors like Flutter taking market share from what would have normally been done in C#.

One area where C# is hobbled is in the area of UI construction, which gave birth to XAML, which generally sucks.

It's unfortunate that C# has required hackish full-suites of extension methods to achieve a notation that competes with Flutter. I think this functionality shouldn't require this much work.

Yes, I agree that too-terse/overloaded statements is bad programming. I tend to go the verbose route, not combining things into single lines (e.g. method(anotherMethod(args)); IMO is normally bad).

However, for UI composition, this domain/context really deserves better native support from the C# language itself.

It doesn't seem to be much of a stretch to simply say "any method can be called from the Object Initializer block" -- and for safety, an order could be enforced such that "Init-Only Properties must always be called first", then after that, anything goes.

This would be: (a) Terse, (b) Clear, and (c) enable UI composition naturally.

It simply works similar to how VB "with" statements used to work, in that all operations are done on the main source object. They of course all operate In-Order. It should be the equivalent of making all the same calls using a local variable. In the Object Initialization block, it would just work like a "with" statement. Very simple, concise, clear, and extremely useful for certain contexts.

For contexts where it's not appropriate, just don't use it. But in short, even if you use it "inappropriately", what's the harm? It works the same as if you just made all the same calls (in the same order) using a local variable assigned to the new object. So it just amounts so a bit of very useful syntactic sugar, that makes UI composition syntax "natural/built-in", rather than an obtuse hack.

I think you are all awesome. Thank you for your attention, and feedback. This has been very encouraging to witness this energy , genius, and tone of the discussions. Kudos to the whole group of you.

najak3d avatar Jan 16 '22 05:01 najak3d

I disagree. I think we do

Then what is the reason to allow general compound assignment, instead of just subscribing events, in object initializer? As I said, I agree that event subscription is important in object initializer, but I don't think allowing it for fields and property is anywhere better than calling methods, at least based on your initialization (or construction) vs mutation logic.

If I understand correctly,

var item = new Item()
{
  Value += 1,
};

does not distinguish whether the modification of Value is inialization or mutation. If you think you do, then the new syntax should only support init-only properties, not all set properties or fields. I know those are for back-compatibility. But that is exactly the problem here. Because of the back-compatibility, C# cannot have a clear difference between initialization and mutation from the callee side, or it will break old callers from modifying fields that is a mutation, not initialization.

Another reason I can think of why you say you do is that you think any modification of a field using compound assignment in object initializer can be seen as initialization instead of mutation. If that is the case, you are defining what is initialization/mutation by where the code is written, and that also means whatever we add to object initializer (e.g. method calls) will also be considered as initialization, which should be fine according to your initialization/mutation standard.

acaly avatar Jan 16 '22 05:01 acaly

It seems to me that "immutability" is currently enforced only by "readonly" modifiers or using "init" instead of "set" for properties.

Restriction: Readonly method - cannot use Init block -- all values must be set using a coded constructor.

Too Loose: Init methods - currently Order-of-ops in Init Block do NOT enforce "init methods must be called first". This seems unfortunate, because in a small way, this does not really enforce immutability, at least during the Init block.

As it currently stands though, the Init Block simply runs everything "in order" as you wrote it. That's pretty simple. So if we want to enforce "immutability" via Init Blocks, then we need to either:

  1. Invent new notation (e.g. "InitFirst") to operate as Property Setter, which enforces order within Init blocks.
  2. OR: Treat it as compiler error where an "Init only" setter is called AFTER anything except non-Init Properties.
  3. OR: Simply make sure "init only" setters are called before any Non-Property Setter. So method calls must come after calls to "Init Only" property setters. (I'd vote for # 3, so that we have no backward compatibility issues.)

So in this simple/sensible fashion, C#11 Init-Blocks can offer better "immutability" support, while also adding very useful syntax for nested composite construction (e.g. UI's). Adds magnificent benefit, without confusion or downside.

najak3d avatar Jan 16 '22 06:01 najak3d

It's unfortunate that C# has required hackish full-suites of extension methods to achieve

this domain/context really deserves better native support from the C# language itself.

You are being completely arbitrary. You reject one 'native' c# feature for being 'hackish', but then want some other features to do the same thing.

Extension methods are native. They're part of the language. Avoiding features that have been around for 15 years and are widespread and fully embraced by the ecosystem just because you don't like them is not going to motivate is to create something new.

CyrusNajmabadi avatar Jan 16 '22 06:01 CyrusNajmabadi

You are being completely arbitrary. You reject one 'native' c# feature for being 'hackish', but then want some other features to do the same thing.

Extension methods are native. They're part of the language. Avoiding features that have been around for 15 years and are widespread and fully embraced by the ecosystem just because you don't like them is not going to motivate is to create something new.

The NATURE of these extensions is hackish. For example, Button already has a "Text" property, but because it does not "return Button", we cannot use it! Therefore we have to create a NEW "Text()" method extension to use in it's place... to do the same thing, only it returns Button."

So it forces 100% replacement of all existing methods/properties, to support this syntax. But if the Object-Init syntax were simply a bit more functional -- then we wouldn't need to use all of these hacked extension methods.

Extension methods to add functionality is a good idea. And I'm glad they are available for scenarios like we are doing now -- because they allow us to "hack C# language" to essentially create a notation not currently supported by C# (but should be supported, IMO).

Extensions that are awesome are ones that extend collections to give you a "Count()" method for an IEnumerable, or the various other Linq extensions. Those are all nice, but are ADDING functionality.

In our case, our methods are 90% NOT adding functionality, but are simply "redoing existing properties/methods" to make up for an inadequacy of the C# language itself (which is supported by Flutter/Dart and others). That's why it's a hack, vs. the many other uses of Extensions, where it's not a hack.

IMO, in a way, though, most usages of Extensions are a bit hackish in nature. In short, remove the "using ExtensionsNamespace" and your code suddenly breaks. So it's a bit hackish/weird, and in many cases have been overused by some. The best we can say about Extensions is that they are VERY helpful in overcoming awkwardness that would otherwise result without them, and so they are "good/useful" and we're glad they exist.

If you can write code that relies upon 1000 Extension methods, vs. writing code that needs almost ZERO extension methods, using nearly the same syntax -- it's preferred, by far, to have the code non-reliant upon these extensions.

In short, I'm not calling the "syntax for UI composition" hackish; but I am calling the current method of using 1000's of extensions to make this syntax possible, "hackish", because it is. But I'm glad this hack works, because at least we do enjoy the benefits of C#-markup-composition syntax. It's just unfortunate that this syntax requires so many awkward extensions to make it possible.

najak3d avatar Jan 16 '22 07:01 najak3d

@najak3d

That's not a case of extension methods being hackish, that's a case of using extension methods to hack C# to smell like some other language because you want the style of that other language. That is the hack, not extension methods.

There is no goal to make C# the language to "rule them all", especially not by blindly copying the syntax and idioms of other languages. C# aims to be a good tool and to offer solutions to problems (note, not necessarily the solution that you'd prefer). If popularity and a user base comes with that, then great. C# has a solid position on the TiOBE index and the 2022 rankings indicate an uptick in usage over the previous year.

HaloFour avatar Jan 16 '22 13:01 HaloFour

The NATURE of these extensions is hackish. For example, Button already has a "Text" property, but because it does not "return Button", we cannot use it!

I don't understand. Why don't you just set the Text property directly (either during construction or after?). It's already a mutable property. What do you need a method for?

CyrusNajmabadi avatar Jan 16 '22 19:01 CyrusNajmabadi

@HaloFour :

That's not a case of extension methods being hackish, that's a case of using extension methods to hack C# to smell like some other language because you want the style of that other language. That is the hack, not extension methods.

There is no goal to make C# the language to "rule them all", especially not by blindly copying the syntax and idioms of other languages. C# aims to be a good tool and to offer solutions to problems (note, not necessarily the solution that you'd prefer). If popularity and a user base comes with that, then great. C# has a solid position on the TiOBE index and the 2022 rankings indicate an uptick in usage over the previous year.

I'm glad we agree that the current methods used by MANY UI's is "hackish" (even Comet, I think is employing such a hack). So to make C# capable of "composing UI's with simple notation", it requires this hack. Extension methods ALLOW the hack. I agree that Extensions themselves aren't innately "a hack", but are often used to create hacks, especially of this nature.

I do think .NET should rule-them-all. The fact that it supports Extensions, has allowed C# to remain competitive against others like Flutter/Dart -- because this "hack" is mostly hidden from those using it. Some 3rdparty generally "provides the extensions" and users don't have to do this for themselves.

For situations where C# is good, i.e. Client apps where you "write it once, and runs everywhere" - C# should rule. But because Microsoft dropped-the-ball for over 10 years, it created a gap now being filled by Flutter/Dart. Flutter/Dart is filling the gap and stealing market share in this domain (e.g. 400,000 new Flutter apps since 2018). MAUI is trying to steal it back, but appears mismanaged or understaffed, and has QA issues and is very late -- AND doesn't even provide a pixel-perfect UI solution (where UI operates/looks the same everywhere, as many apps prefer - i.e. one-user-manual).

So for client-apps, that need to run everywhere, C# should rule, without question (around 2006-2010 time frame, most new apps of this nature were written in C#; but now Xamarin-Forms has almost become a desert wasteland, in comparison).

Client-App development has been grossly trending towards MVU and UI's created in Code, not XAML. C# requires a hack to do this, while many other languages do not. IMO, this has become a clear C# deficiency, in the context where C# should rule.

najak3d avatar Jan 16 '22 20:01 najak3d

@najak3d

I'm glad we agree that the current methods used by MANY UI's is "hackish" (even Comet, I think is employing such a hack).

No, I don't agree with this. The hack is that the frameworks are intentionally avoiding the idiomatic approach for initializing types that has been provided in .NET and C# for 20+ years.

Simply put, stop trying to force the idioms of one language into another and you won't run into the friction. That applies for any two languages or frameworks.

HaloFour avatar Jan 16 '22 20:01 HaloFour

@najak3d

Flutter/Dart is filling the gap and stealing market share in this domain (e.g. 400,000 new Flutter apps since 2018)

Dart barely registers on the TiOBE index despite being around for over a decade. COBOL and VBScript rank higher. Even if language design was a popularity contest, which it is not, C# certainly has nothing to fear in this space. C# is not going to turn into Dart to compete with Dart.

HaloFour avatar Jan 16 '22 20:01 HaloFour

@najak3d you still haven't provided any good examples of use cases these new language features are needed for. You've just claimed over and over again that they're needed. This is not an effective argument.

I get that you think flutter is great and that MS needs to do more to complete there. But that's not an argument that means that the language needs to change. We could potentially do everything you've mentioned here in the language and not move the needle one iota.

We need solid data (and real use cases) that demonstrate that it is the language side which is the problem here and that this is the right actually solution to that problem.

Simply claiming it is by Fiat, and ignoring the counterarguments about how your existing use cases can already be solved is not helping your case.

Please show real world cases that need this.

CyrusNajmabadi avatar Jan 16 '22 20:01 CyrusNajmabadi