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] Extend with expression to anonymous type #3530

Open
3 of 4 tasks
leandromoh opened this issue Jun 3, 2020 · 31 comments
Open
3 of 4 tasks

[Proposal] Extend with expression to anonymous type #3530

leandromoh opened this issue Jun 3, 2020 · 31 comments
Assignees
Labels
Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification Proposal champion Smallish Feature
Milestone

Comments

@leandromoh
Copy link
Contributor

leandromoh commented Jun 3, 2020

Extend with expression to anonymous type

  • Proposed
  • Prototype: Done.
  • Implementation: Done,
  • Specification: Not Started

Speclet: https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/record-structs.md

Summary

The with expression, introduced in C# 9, is designed to produce a copy of the receiver expression, in a "non-destructive mutation" manner.

This proposal extend with expression to anonymous type, since they are also immutable, the feature may fit well with them too.

Note: F# has a very similar feature called copy and update record expressions.

Motivation

Reduce boilerplate code to create new instances of anonymous type based on already existing instance.

Current approach:

var person = new { FirstName = "Scott", LastName = "Hunter", Age = 25 };

var otherPerson = new { person.FirstName, LastName = "Hanselman", person.Age };

Proposed:

var person = new { FirstName = "Scott", LastName = "Hunter", Age = 25 };

var otherPerson = person with { LastName = "Hanselman" };

Detailed design

The syntax is the same described in with expression section of the record proposal, that is

with_expression
    : switch_expression
    | switch_expression 'with' '{' member_initializer_list? '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

In the context of this proposal, the receiver expression must be an anonymous object. Also, different of the original with proposal, the anonymous type will not need contains an accessible "clone" method, since the copy can be done by the compiler just calling the constructor of the anonymous type, what maintain how anonymous types are emitted.

Currently, each anonymous type's property has a correspondent parameter in the type constructor, with same name and type. So compiler must pass the correspondent member expression for each argument. Case the member exists in the member_initializer_list, compiler must pass as equivalent argument the expression on the right side of the member_initializer.

The orders each member_initializer appears is irrelevant, since they will be processed in constructor call, therefore in the order of the parameters.

Drawbacks

None.

Alternatives

One workaround is use reflection and expression trees to build such anonymous type's constructor call. However theses features have performance cost and are, perhaps, not common for all programmers of the language. It also requires new intermediary anonymous instances to represent the properties and values that we will be changed. Here is a gist with my workaround with follow usage:

var otherPerson = person.With(new { LastName = "Hanselman" });

Unresolved questions

Design meetings

@leandromoh leandromoh changed the title [Proposal] Extend with operator to anonymous type [Proposal] Extend with expression to anonymous type Jun 3, 2020
@leandromoh
Copy link
Contributor Author

In case of someone is looking for such feature, here is a gist with my workaround. with follow usage:

var otherPerson = person.With(new { LastName = "Hanselman" });

@333fred
Copy link
Member

333fred commented Jun 9, 2020

I'm willing to champion this extension, with the caveat that it won't come in 9 :)

@HaloFour
Copy link
Contributor

HaloFour commented Jun 9, 2020

Makes sense, you could probably consider anonymous types to automatically be records given they already have value equality, no?

@333fred
Copy link
Member

333fred commented Jun 9, 2020

Possibly. It would still depend on whether we add anything else to the definition of a record type. I'm not sure whether we'd add a copy constructor, for example, or if we'd just do the copy at call site.

@333fred
Copy link
Member

333fred commented Jun 9, 2020

@leandromoh this will need an actual spec. Would you be willing to take a stab at writing one? It would need to be an extension to the existing records proposal.

@HaloFour
Copy link
Contributor

HaloFour commented Jun 9, 2020

Would "withers" come for free if an anonymous type definition was modified so that:

  1. Every property was { get; init; } instead of { get; }, and
  2. The anonymous type exposed a Clone method which would create a shallow clone of the anonymous type?

@333fred
Copy link
Member

333fred commented Jun 9, 2020

Probably, but then every piece of code generates an anonymous type would need to be recompiled to work with this feature.

@svick
Copy link
Contributor

svick commented Jun 9, 2020

@333fred Since anonymous types can't be exposed publicly, why would that be a problem?

@333fred
Copy link
Member

333fred commented Jun 9, 2020

I suppose it might be fine. Still, we need a spec to actually evaluate this :)

@leandromoh
Copy link
Contributor Author

leandromoh commented Jun 9, 2020

@leandromoh this will need an actual spec. Would you be willing to take a stab at writing one? It would need to be an extension to the existing records proposal.

@333fred I would like to do it, of course. I just need to know exactly what and how to do it.
if someone explain me, I will be glad to contribute more.

@333fred
Copy link
Member

333fred commented Jun 9, 2020

Basically, look at the existing records proposal (https://github.com/dotnet/csharplang/blob/master/proposals/records.md#with-expression) and detail exactly how it would need to be modified in order to support withing an anonymous type. How would it actually function? Would we make changes to how anonymous types are emitted as @HaloFour suggested to generate a copy constructor & mark the properties as init, or would we do codegen at the with invocation site? I think the former approach has promise, but it needs to be investigated and formalized. The proposal template is here: https://github.com/dotnet/csharplang/blob/master/proposals/proposal-template.md. Note: don't make a PR yet, just filling it out in this issue is fine for now.

@HaloFour
Copy link
Contributor

HaloFour commented Jun 9, 2020

Given that the compiler has complete control over the definition and use of anonymous types it might make more sense to have the compiler treat it special and just emit its own call to the constructor manually copying the source properties, as suggested in the original post. I don't see any other added benefit to adding/changing the members of the anonymous type, it's not like external code can take advantage of it outside of reflection.

@ChayimFriedman2
Copy link

Will it allow adding properties, since anonymous types do not have declared type like records?

@HaloFour
Copy link
Contributor

@ChayimFriedman2

That sounds interesting, although I'd a little concerned that it would make it very easily to make a mistake:

var person1 = new { Name = "Bill", Age = 45 };
var person2 = person1 with { AGe = 46 }; // oops!

@ChayimFriedman2
Copy link

Right. That seems like VB's implicit variable declaration, which is known to be error-prone...

@leandromoh
Copy link
Contributor Author

@333fred @HaloFour proposal added in the description of this issue. Feel free to edit it.

@AartBluestoke
Copy link
Contributor

@HaloFour perhaps 'anon with' {x=y} requires x to be an already existing variable (ie, you can't extend an anon type with 'with')

@333fred
Copy link
Member

333fred commented Jun 16, 2020

@leandromoh it's a good start, but it needs to be more concrete. A spec isn't a guess about how to implement something: it lays out exactly how it will be implemented.

@leandromoh
Copy link
Contributor Author

leandromoh commented Jun 23, 2020

@333fred okay, I will try detail it more, but probably it will need some adjusts of you after all.

@leandromoh
Copy link
Contributor Author

leandromoh commented Jun 23, 2020

@ChayimFriedman2 @HaloFour javascript has a spread operator that makes exactly this. example:

var obj1 = { name: "bob",  type:"Apple" };
var obj2 = { name: "joe", price: 0.20 }; 
var merge = {...obj1, ...obj2, age: 34 }; 

console.log(merge); 
/* { 
   age: 34
   name: "joe"
   price: 0.2
   type: "Apple"
}; */

If objects have common properties the priority will be right to left. The object the most at the right will have priority over the one at its left and so on. If we could expand C#'s anonymous type in a similar way would be amazing.

@leandromoh
Copy link
Contributor Author

leandromoh commented Jun 24, 2020

@333fred @HaloFour "Detailed design" section updated in the description. Hope it is closer of what is need.

@leandromoh
Copy link
Contributor Author

For note: with expressions on non records on LDM-2020-06-22

@333fred 333fred added this to the 10.0 candidate milestone Jul 20, 2020
@333fred 333fred modified the milestones: 10.0 candidate, 10.0 Working Set Sep 28, 2020
@MgSam
Copy link

MgSam commented Sep 29, 2020

If this were pre-C#-7.0 I would think this was a great idea. Post-C#-7.0 I don't see any point of putting development work into anonymous types. Tuples are better in nearly every respect - they are structs, they can be return types and parameter types, and if you really really need to mutate them, you already can.

Given that, how often, and why, do you really need to "mutate" an anonymous type? The use cases for this seem vanishingly small.

@leandromoh
Copy link
Contributor Author

leandromoh commented Oct 1, 2020

@MgSam this proposal enables anonymous types practically act like anonymous record types, as they have the rest of the properties of a record already: value equality, ToString, etc.

F# already have anonymous record types.

@JKamsker
Copy link

JKamsker commented Jan 13, 2021

It would be also cool if those with operators could extend the anonymous type.
My usecase:

var files = Directory.EnumerateFiles(dir, "*.*", SearchOption.TopDirectoryOnly)
  .Select(x => new FileInfo(x))
  .Select(x => new
  {
      File = x,
      FileName = x.Name,
      Match = myRegex.Match(x.Name)
  })
  .Where(x => x.Match.Success)
  .Select(x => x with
  {
      Group = x.Match.Groups[1].Value,
      FileNumber = x.Match.Groups[2].Value
  }));

The Resulting anonymous type should then contain

File
FileName
Match
Group
FileNumber

@jcouv jcouv modified the milestones: Working Set, 10.0 Jul 9, 2021
@333fred 333fred added the Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification label Jan 11, 2022
@darkflame0
Copy link

darkflame0 commented Aug 7, 2022

It would be also cool if those with operators could extend the anonymous type. My usecase:

var files = Directory.EnumerateFiles(dir, "*.*", SearchOption.TopDirectoryOnly)
  .Select(x => new FileInfo(x))
  .Select(x => new
  {
      File = x,
      FileName = x.Name,
      Match = myRegex.Match(x.Name)
  })
  .Where(x => x.Match.Success)
  .Select(x => x with
  {
      Group = x.Match.Groups[1].Value,
      FileNumber = x.Match.Groups[2].Value
  }));

The Resulting anonymous type should then contain

File
FileName
Match
Group
FileNumber

any progress on this?

my use case is flattening groupby data

var group = data.GroupBy(a=>a.KeyProp).Select(a=>new {KeyProp = a.Key, Items = new {...}});
var flat = group.SelectMany(a=>a.Items.Select(b=>b with {Key = a.KeyProp}));

@darkflame0
Copy link

darkflame0 commented Aug 7, 2022

but using with expression for extending objects can be misleading.
meybe this is better?

@ChayimFriedman2 @HaloFour javascript has a spread operator that makes exactly this. example:

var obj1 = { name: "bob",  type:"Apple" };
var obj2 = { name: "joe", price: 0.20 }; 
var merge = {...obj1, ...obj2, age: 34 }; 

console.log(merge); 
/* { 
   age: 34
   name: "joe"
   price: 0.2
   type: "Apple"
}; */

If objects have common properties the priority will be right to left. The object the most at the right will have priority over the one at its left and so on. If we could expand C#'s anonymous type in a similar way would be amazing.

var flat = group.SelectMany(a=>a.Items.Select(b=>new {Key = a.KeyProp, ...b}));

@qrli
Copy link

qrli commented Aug 8, 2022

Introducing spread operator would be much bigger feature. And there would be complicated cases where obj1 and obj2 have properties with the same name but different types, for example.

Here using with is a natural extension of an existing feature, with well defined behavior.

@mrwensveen
Copy link

@ChayimFriedman2

That sounds interesting, although I'd a little concerned that it would make it very easily to make a mistake:

var person1 = new { Name = "Bill", Age = 45 };
var person2 = person1 with { AGe = 46 }; // oops!

On the other hand, the analogous F# feature does allow exactly this on anonymous record types.

@JKamsker
Copy link

@ChayimFriedman2

That sounds interesting, although I'd a little concerned that it would make it very easily to make a mistake:

var person1 = new { Name = "Bill", Age = 45 };
var person2 = person1 with { AGe = 46 }; // oops!

On the other hand, the analogous F# feature does allow exactly this on anonymous record types.

Such errors can be avoided by using intellisense, which should know existing anonymous properties.

@AartBluestoke
Copy link
Contributor

Such errors can be avoided by using intellisense, which should know existing anonymous properties.

It feels like extension with members that differ only by case should be a warning that needs to be suppressed to do deliberately ...

Is "this should be a warning" a language or implementation discussion?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Implemented Needs ECMA Spec This feature has been implemented in C#, but still needs to be merged into the ECMA specification Proposal champion Smallish Feature
Projects
None yet
Development

No branches or pull requests