Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 of 4 tasks
CyrusNajmabadi opened this issue Sep 10, 2021 · 111 comments
Open
1 of 4 tasks
Assignees
Milestone

Comments

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Sep 10, 2021

Compound assignment in object initializer and with expression

  • 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

@wrexbe
Copy link

wrexbe commented Sep 11, 2021

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?

@jnm2
Copy link
Contributor

jnm2 commented Sep 11, 2021

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?

@wrexbe
Copy link

wrexbe commented Sep 11, 2021

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.

@bartdesmet
Copy link

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 */ };

@CyrusNajmabadi
Copy link
Member Author

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.

@jnm2
Copy link
Contributor

jnm2 commented Sep 14, 2021

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.

@bernd5
Copy link
Contributor

bernd5 commented Sep 14, 2021

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

@333fred 333fred added this to the Backlog milestone Sep 21, 2021
@TahirAhmadov
Copy link

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
Copy link

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],
};

@jnm2
Copy link
Contributor

jnm2 commented Nov 3, 2021

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

@CyrusNajmabadi CyrusNajmabadi changed the title [Proposal]: Event hookup in object initializer [Proposal]: Compound assignment in object initializer and with expression Jan 2, 2022
@najak3d
Copy link

najak3d commented Jan 16, 2022

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.

@CyrusNajmabadi
Copy link
Member Author

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.

@acaly
Copy link
Contributor

acaly commented Jan 16, 2022

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".

@CyrusNajmabadi
Copy link
Member Author

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.

@najak3d
Copy link

najak3d commented Jan 16, 2022

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()".

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?

@theunrepentantgeek
Copy link

theunrepentantgeek commented Jan 16, 2022

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?

@CyrusNajmabadi
Copy link
Member Author

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
Copy link
Member Author

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
} 

@theunrepentantgeek
Copy link

theunrepentantgeek commented Jan 16, 2022

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.

@najak3d
Copy link

najak3d commented Jan 16, 2022

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.

@acaly
Copy link
Contributor

acaly commented Jan 16, 2022

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.

@najak3d
Copy link

najak3d commented Jan 16, 2022

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.

@CyrusNajmabadi
Copy link
Member Author

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.

@najak3d
Copy link

najak3d commented Jan 16, 2022

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
Copy link

najak3d commented Feb 6, 2022

@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
Copy link
Member Author

CyrusNajmabadi commented Feb 6, 2022

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
Copy link

najak3d commented Feb 6, 2022

@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
Copy link
Contributor

HaloFour commented Feb 6, 2022

@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
Copy link

jmarolf commented Feb 6, 2022

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.

  • plain old data (POD)
  • "rich" objects

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
Copy link

jmarolf commented Feb 6, 2022

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
Copy link
Member Author

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.

@jmarolf
Copy link

jmarolf commented Feb 6, 2022

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

@jmarolf
Copy link

jmarolf commented Feb 6, 2022

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
Copy link

najak3d commented Feb 7, 2022

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
Copy link

najak3d commented Feb 7, 2022

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
Copy link

najak3d commented Feb 7, 2022

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

#5727

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

@najak3d
Copy link

najak3d commented Feb 7, 2022

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
Copy link
Contributor

HaloFour commented Feb 7, 2022

@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
Copy link

jmarolf commented Feb 7, 2022

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
Copy link

najak3d commented Feb 7, 2022

@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?

@najak3d
Copy link

najak3d commented Feb 7, 2022

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
Copy link
Contributor

HaloFour commented Feb 7, 2022

@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
Copy link

najak3d commented Feb 7, 2022

@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
Copy link
Contributor

HaloFour commented Feb 7, 2022

@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
Copy link

najak3d commented Feb 7, 2022

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
Copy link

najak3d commented Feb 7, 2022

@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
Copy link
Contributor

HaloFour commented Feb 7, 2022

@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
Copy link

Just confirming, this will work, right?

var x = new Class { StrProp += " suffix" };

@333fred
Copy link
Member

333fred commented May 4, 2022

I would think so.

@msedi
Copy link

msedi commented Aug 19, 2022

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
Copy link

totszwai commented Sep 18, 2023

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

/subscribed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests