fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
346 stars 21 forks source link

Support 'without' for Anonymous Records #762

Open cartermp opened 5 years ago

cartermp commented 5 years ago

I propose we support expressions of the following:

let a = {| X = 1; Y = 2; Z = 3|}
let a' = {| a without Y |} // {| X = 1; Z = 3 |}

That is, being able to construct a new anonymous record that is a subset of another one.

The existing way of approaching this problem in F# is to manually construct a':

let a = {| X = 1; Y = 2; Z = 3|}
let a' = {| X = a.X; Z = a.Z |} // {| X = 1; Z = 3 |}

Pros and Cons

The advantages of making this adjustment to F# are:

The disadvantages of making this adjustment to F# are :

Extra information

Here's what the RFC says about this:

Supporting "smooth nominalization" means we need to carefully consider whether features such as these allowed:

  • removing fields from anonymous records { x without A }
  • adding fields to anonymous records { x with A = 1 }
  • unioning anonymous records { include x; include y }

These should be included if and only if they are also implemented for nominal record types. Further, their use makes the cost of nominalization higher, because F# nominal record types do not support the above features - even { x with A=1 } is restricted to create objects of the same type as the original x, and thus multiple nominal types will be needed where this construct is used.

However, Anonymous Records already support {| x with SomethingElse = foo |} to construct a new AR that has more fields than the one it was constructed from. This means that the middle point is already sort of violated, since you cannot reproduce this with record types.

Estimated cost (XS, S, M, L, XL, XXL): S-M

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

rmunn commented 3 years ago

@auduchinok There's also a functionality difference between your proposal and the without proposal.

let frontendUser = {| backendUser without password |}
// Or
let frontendUser2 = {| backendUser with firstName, lastName, email |}

If backendUser later grows an avatarUrl field that we want to include in frontendUser, the first line will auto-include the new field with no code change needed, while the second line will need to be manually updated. And since these are anonymous records, the type system can't help you find all the places where avatarUrl now needs to be included.

vzarytovskii commented 3 years ago

Yeah, with- looks terrible. It's just saving 2 (TWO!) characters from without and it introduces something that looks terrible, and confusing to users.

I'm playing with it now, and yeah, it really looks weird :) Maybe it will be a good idea to iterate on it again.

However, the idea was not to save characters, but to not use currently valid identifier (without) as a new reserved keyword, since it will technically be a breaking change (and has to be hidden under the lang version) :(

jcmrva commented 3 years ago

The more I think about it, the more I like not with.

cartermp commented 3 years ago

Can we move discussion here? https://github.com/fsharp/fslang-design/discussions/616#discussioncomment-1130567

The RFC is merged so there's not much to talk about in this thread.

vzarytovskii commented 3 years ago

Can we move discussion here? fsharp/fslang-design#616 (comment)

The RFC is merged so there's not much to talk about in this thread.

Shall this issue be locked and closed (since RFC is approved and merged) in favour of discussion?

cartermp commented 3 years ago

Nah, we can just keep it as-is for now. I think the tendency is to close only when the feature is merged and in preview.

Lanayx commented 1 year ago

Another idea - what about this syntax (association with destructors)

let x = {| A =1; B = 2 |}
let y = {| x with ~A |}
smoothdeveloper commented 8 months ago

wondering if with considerations around #1253, and this, we could have way to remove arbitrary field for any anonymous record, with inline function, rather than having to define it for a specific type.

let inline justBe (be: {|manner:'a; ...|}) = {| be without manner |}