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

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.

Correct. It has it's downsides. For my experience, the Scene could also have Custom Code, already. And so there truly was nothing you could accomplish in this sandboxed C# that you couldn't already accomplish via custom C#. So for this scenario, that type of security wasn't an issue.

For MOST situations, you are 100% correct. Which is surely a major reason why it's not used much.

In our case -- to achieve reasonable security, we just compiled the code run-time, so that we could do appropriate checks and confined the compiler environment to not-compile insidious code. And so we serialized the Text, not the DLL directly.

That said -- this makes succinct syntax even more attractive for this scenario.

HaloFour commented 2 years ago

@najak3d

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?

No, this is still very much a concern for the library. I understand that you're trying to make this point in the context of your preferred Dart/Flutter way of doing things, but given designers have existed in the .NET ecosystem since inception I think that argument is going to fall flat. Designers don't have a problem in this scenario.

najak3d commented 2 years ago

@najak3d

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?

No, this is still very much a concern for the library. I understand that you're trying to make this point in the context of your preferred Dart/Flutter way of doing things, but given designers have existed in the .NET ecosystem since inception I think that argument is going to fall flat. Designers don't have a problem in this scenario.

More succinct code, so long as it is just as clear - is preferable to more bloated syntax. That's the main point of what I'm saying here. The designer can either generated bloated C# or succinct C# to achieve the same end goal. The succinct notation, is likely superior, so long as it is equally intelligible. In this case, of including .method() calls into the Init Block, it is equally intelligible, and IMO, superior.

It seems a great many things introduced into the C# language have been done despite being able to say "XYZ has existed in the .NET ecosystem for ABC years". The existence of people "dealing with how things are" doesn't mean you therefore don't try to make improvements. Most of the C# changes I've seen qualify as these types of changes; yet they are done anyways, AND were good improvements.

HaloFour commented 2 years ago

@najak3d

More succinct code, so long as it is just as clear - is preferable to more bloated syntax.

If it's being compiled as a part of the project then it doesn't really matter, as long as it's clear. The WPF designer doesn't have a problem here, and the language isn't going to adopt new syntax for the sake of making life slightly easier for designer code that is likely much easier to emit without trying to construct a complex fluent graph anyway.

najak3d commented 2 years ago

Like for Records, and concerns of immutability -- you could achieve this by making constructors that set all of the private fields, and then mark them "readonly". Why do more? Because doing more is helpful and beneficial by making it easier for us to have immutable classes.

najak3d commented 2 years ago

@najak3d

More succinct code, so long as it is just as clear - is preferable to more bloated syntax.

If it's being compiled as a part of the project then it doesn't really matter, as long as it's clear. The WPF designer doesn't have a problem here, and the language isn't going to adopt new syntax for the sake of making life slightly easier for designer code that is likely much easier to emit without trying to construct a complex fluent graph anyway.

I do agree that this is a minor concern. Mostly because very little serialization is done in this fashion, and where it is being done, making the code shorter doesn't really help much.

I stepped into this because someone else suggested that "Fluent API might interfere with serialization concerns" -- and my response was simply "if anything, it can only help". My proposal poses zero threat to his concerns over serialization.

The rest was just a brainstorm based on experience... but you are right, that example is not a significant concern for us here.

HaloFour commented 2 years ago

@najak3d

I stepped into this because someone else suggested that "Fluent API might interfere with serialization concerns" -- and my response was simply "if anything, it can only help". My proposal poses zero threat to his concerns over serialization.

I think you misinterpreted that statement, this has nothing to do with designers. It has to do with how a serialization library would be able to interpret the use of these kinds of methods when it comes to deserializing from a wire format. That is based entirely on reflection and there is no intermediate source in play.

TahirAhmadov commented 2 years ago

Just confirming, this will work, right?

var x = new Class { StrProp += " suffix" };
333fred commented 2 years ago

I would think so.

msedi commented 2 years ago

Addtionally I would add some scenario that came up today:

I have a read-only structure where properties are dependent from other properties in the same structure. It would be completely sufficient to have everything set in the initialization using a compound initialization with init it is not possible for several reasons, because in the example order might play a role, so I cannot access the Blocksize within the initialization:

 var gridSize1 = new CudaGridBlockSize
            {
                BlocksizeX = 64,
                BlocksizeY = 1,
                BlocksizeZ = 1,
                GridsizeX = ((width - 1) / BlocksizeX / 4) + 1,
                GridsizeY = ((height - 1) / BlocksizeY) + 1,
                GridsizeZ = ((depth - 1) / BlocksizeZ) + 1
            } 

so I came up with the idea of using withers but found that also here I have no access to the original source context is lost:

 var gridSize1 = new CudaGridBlockSize
            {
                BlocksizeX = 64,
                BlocksizeY = 1,
                BlocksizeZ = 1
            } 
            with
            {
                // Accessing gridsize1 does not work
                GridsizeX = ((width - 1) / gridSize1.BlocksizeX / 4) + 1,
                GridsizeY = ((height - 1) / gridSize1.BlocksizeY) + 1,
                GridsizeZ = ((depth - 1) / gridSize1.BlocksizeZ) + 1
            };

The current solution would be:

 var gridSize1 = new CudaGridBlockSize
            {
                BlocksizeX = 64,
                BlocksizeY = 1,
                BlocksizeZ = 1
            } ;
  gridSize1 = gridSize1 with
            {
                GridsizeX = ((width - 1) / gridSize1.BlocksizeX / 4) + 1,
                GridsizeY = ((height - 1) / gridSize1.BlocksizeY) + 1,
                GridsizeZ = ((depth - 1) / gridSize1.BlocksizeZ) + 1
            };

which is not super complex but if you are working on this it might be something that also fits into this topic.

totszwai commented 1 year ago

Came here from an 2014 SO thread LOL... Would be really nice to have such feature available, especially for those event callbacks. 😭

/subscribed

acple commented 2 months ago

This is exactly the feature I've been waiting for. But how about Increment/Decrement operators? These are still useful in some common scenarios. Isn't it in scope?

var countUp = myRecord with { Count++ };
markusschaber commented 1 month ago

This is exactly the feature I've been waiting for. But how about Increment/Decrement operators? These are still useful in some common scenarios. Isn't it in scope?

var countUp = myRecord with { Count++ };

I think allowing postincrement and postdecrement operators may be confusing, as they usually return the previous state as expression result, and increment the source variable. A postincrement as above could be interpreted as create countUp with the same count than myRecord, and then increment myRecord.Count. A similar problem for preincrement and predecrement, both records would end up with the new value. Apart form the fact that, when myRecord is read only, the property cannot be modified at all.

snarbies commented 1 month ago

Just my two cents...

could be interpreted as create countUp with the same count than myRecord, and then increment myRecord.Count.

I don't see why you wouldn't read this the same way then.

var countUp = myRecord with { Count += 1 };

They both convey exactly the same mutation on exactly the same storage location. I take it to be an issue of how we intuitively read it, but I think the fact that it's part of a with expression sufficiently clarifies.

jnm2 commented 1 month ago

++Count means the same as Count += 1, but Count++ has a slight difference:

var count = 0;
Console.WriteLine(count += 1); // 1
Console.WriteLine(++count); // 2
Console.WriteLine(count++); // Still 2
snarbies commented 1 month ago

They can mean something slightly different, yes, but not in this situation.

snarbies commented 1 month ago

Well... I can kind of see where it depends on how you look at it. But one interpretation makes sense, and one does not. We aren't keeping the value of the "expression", we're keeping the side-effect.

In other words, if you interpret with {count++} to mean to modify the source, then how could you not interpret with {count += 1} the same way? They're both modifying the same thing.