fsharp / fslang-suggestions

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

Support for F# record syntaxes for C# defined records #1138

Open smoothdeveloper opened 2 years ago

smoothdeveloper commented 2 years ago

I propose we add support to F# language for

to C# defined record types.

Pros

Cons

Extra information

Estimated cost: M

Related suggestions:

https://github.com/dotnet/fsharp/issues/13007#issuecomment-1115259153 (C# X support in F# Y issue) https://github.com/dotnet/csharplang/discussions/6067 (co issue for C# compiler, discussions revolves around F# adjusting code generation for F# records to make it look like what C# compiler expects)

vzarytovskii commented 2 years ago

It sounds reasonable to me, and should be straightforward as long as the scope is only about the with and init.

It will, however, get complicated, if we want full compatibility, such as "converting" C# records to F# ones and vice versa, we will need to deal with the inheritance, IL representation, etc then.

It makes sense from the language perspective to support it, especially for new users, @dsyme wdyt?

smoothdeveloper commented 2 years ago

@vzarytovskii, thanks!

I probably miss other features in the top bullet list that need to be considered, but could you expand on what you mean about "converting" as I am not sure which feature or aspect it relates to, I kind of connect it to the way anonymous records can be extended by adding properties, making a new type.

Is it this particular area of the language?

I am personally ok with limiting the feature scope to what is easiest to tackle, if it allows to cover what felt most needed in the initial list I've put.

This request is not about supporting inheritance of F# records, but I see that consuming the C# ones involves complexity related to records possibly forming a hierarchy.

dsyme commented 2 years ago

I'm inclined not to do "type inference on record field names" for C# record types.

My initial thought is not do "record constructor syntax { X = 1; Y = 2 }" for C# record types either. The construction syntax for C# records is R(X=1, Y=2) and works today.

I'm in two minds about "copy-and-update". Let's assume construction syntax for C# records is R(X=1, Y=2). That seems reasonable given it's so close to what's in C#. If so it just seems odd that we would switch to F# record braces syntax for copy-and-update. Why not r.With(X=1) for example?

That said, it's not a huge problem if we support { r with X = 1 }. But

  1. I guess people will expect { X = 1; Y = 2 } as well....
  2. We should be aware that { a with P=expr} becomes polymorphic syntax potentially applicable to a wider variety of things. That's ok but leads to more questions, like should it be usable with anonymous records? (currently you must use {| a with X = 1 |}).
dsyme commented 2 years ago

As an aside, just noting a technical matter: For { csharpRecord with P=1 } a generic record type can't change generic parameter. This is the same as for F# records:

type R<'T> = { X : 'T; Id: string }
let r1 = { X = 1; Id = "a" } // R<int>
{r1 with X = "one" }  // doesn't become a R<string>, instead an error is given.

Because of the way C# compiles with to a <Clone>$ method there is no scope to allow a change-of-generic-type, because the method is an instance method that returns precisely the same type as the instance object passed in.

From the PL design perspective in theory this could become a different type, but in F# and OCaml it doesn't. F# does allow this for anonymous records:

let r1 = {| X = 1; Id = "a" |} // {| X: int; Id: string |}
{| r1 with X = "one" |}  // {| X: string; Id: string |}
dsyme commented 2 years ago

There's another question whether C# records can be used as inputs to anonymous record syntax, e.g.

{| csharpRecord with Z = 1 |}

F# records can be used as inputs to anonymous records. So this should be possible for C# records too:

type R = { X : int; Id: string }
let r1 = { X = 1; Id = "a" } // R<int>
{| r1 with X = "one" |}
smoothdeveloper commented 2 years ago

@dsyme, a minor comment on

The construction syntax for C# records is R(X=1, Y=2) and works today.

I think R(1,2) also works, and it is kind of not great. IMO, it should be giving a warning similar to https://github.com/fsharp/fslang-design/blob/main/drafts/FS-1095-requirenamedargumentattribute.md

Ideally, it should turn into record initialiser syntax proper, and other syntaxes should give warnings asking to use that syntax only.

Based on discussion on the C# co-thread (not gonna happen, unless F# code gen adjusts), the underlying code-gen for C# records would need to use the same Clone thing, I think this increases the complexity of what I envisioned before having those discussions.

All points you give and that were made on the C# co-thread are great insight of the subtleties into supporting this.

vzarytovskii commented 2 years ago

@dsyme, a minor comment on

The construction syntax for C# records is R(X=1, Y=2) and works today.

I think R(1,2) also works, and it is kind of not great. IMO, it should be giving a warning similar to https://github.com/fsharp/fslang-design/blob/main/drafts/FS-1095-requirenamedargumentattribute.md

I think it's due to C# records generating the appropriate constructor (for positional properties only).

Ideally, it should turn into record initialiser syntax proper, and other syntaxes should give warnings asking to use that syntax only.

I guess this is one the controversial things, whether we want F# record syntax for C# records.

Based on discussion on the C# co-thread (not gonna happen, unless F# code gen adjusts), the underlying code-gen for C# records would need to use the same Clone thing, I think this increases the complexity of what I envisioned before having those discussions.

Maybe I'm mistaken about what you mean here, but if i understood it correctly - i think generating Clone for F# records (to have them compatible with C# with syntax) is a reasonable thing to do.

dsyme commented 2 years ago

I think R(1,2) also works, and it is kind of not great. IMO, it should be giving a warning similar to https://github.com/fsharp/fslang-design/blob/main/drafts/FS-1095-requirenamedargumentattribute.md

My understanding is that these are positional parameters in C#, and so positional invocation is ok. Named properties in C# are R() { X = 1, Y = 2 }

Ideally, it should turn into record initialiser syntax proper, and other syntaxes should give warnings asking to use that syntax only.

I'm not sure about this. Record syntax in F# is not particularly natural particularly for the positional parameters. For things combining positional and named the constructor syntax is already well set up and working well.

dsyme commented 2 years ago

Yes, we can generate <Clone>$, init, reqd, I don't see why not.

smoothdeveloper commented 2 years ago

@vzarytovskii: I guess this is one the controversial things, whether we want F# record syntax for C# records.

@dsyme My understanding is that these are positional parameters in C#, and so positional invocation is ok. Named properties in C# are R() { X = 1, Y = 2 }

To me, similar construct should flow simply, with niceties / idioms of the language they are consumed from, giving the "compiler is your friend" and "language is simple" feel.

True, they are positional in C#, but since F# doesn't allow usage of positional parameter for construction of F# defined records, it really should not encourage it for C# defined records, IMO.

Now, if I read @dsyme comments properly, it assumes that F# will distinguish C# records and expose .With object syntax for non destructive mutation, this remain idiomatic to C#, but I am unsure the distinction is worth it, if we see no issue with endorsing the same syntax we have for F# defined records.

@vzarytovskii: Maybe I'm mistaken about what you mean here, but if i understood it correctly - i think generating Clone for F# records (to have them compatible with C# with syntax) is a reasonable thing to do. @dsyme: Yes, we can generate $

This will make C# enable the with syntax on records defined in both languages, and also, expose .With object syntax to F# if we'd not go the (simpler to me) other route.

Overall, isn't it keeping F# simpler, if records still look like records, no matter which language it is defined in? with minor caveats for things that can be considered side cases (polymorphism due to inheritance and generic).

@dsyme: I'm inclined not to do "type inference on record field names" for C# record types.

This may be a separate suggestion, and of course, mandated by F# adopting the initializer syntax and discouraging the positional constructor argument one.

What, overall, makes it important / better to keep distinction?

It feels with what @dsyme proposes, it would make consumption of F# and C# defined records in C# natural, and make consumption of records not defined in F# a bit alien in F#, with more "object" feel than seems required to me.

Just trying to understand better the motives, and having better code gen for F# records to be consumed in C# is nice move.

smoothdeveloper commented 2 years ago

Another point to consider is pattern matching, which has special syntax support in F#.

Related: #968, caveat: https://github.com/fsharp/fslang-suggestions/issues/968#issuecomment-854072613

@dsyme: I actually really dislike the use of { ... } in patterns, I think it's always really hard to read, kind of unpleasant on the eye, and I sort of regret having it in F# at all...