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

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

Open
smoothdeveloper opened this issue May 3, 2022 · 11 comments
Open

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

smoothdeveloper opened this issue May 3, 2022 · 11 comments

Comments

@smoothdeveloper
Copy link
Contributor

smoothdeveloper commented May 3, 2022

I propose we add support to F# language for

  • copy-and-update for records { myRecord with ... }
  • record constructor syntax { X = 1; Y = 2 }
  • type inference on record field names

to C# defined record types.

Pros

  • better mapping of the same concepts, so they can be used idiomatically in respective languages (from F# side in this suggestion)
  • less friction in multi-language codebases
  • reinforces the "simple F#" feel

Cons

  • adding logic in F# compiler, which is tied to knowledge of C# codegeneration idioms: need to keep track of potential changes that would impact this support as C# compiler evolves
  • adding some complexity in the components currently supporting the record syntax bits in the type checker: longer compile times

Extra information

Estimated cost: M

Related suggestions:

dotnet/fsharp#13007 (comment) (C# X support in F# Y issue)
dotnet/csharplang#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
Copy link

vzarytovskii commented May 3, 2022

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

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

dsyme commented Jul 12, 2022

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

dsyme commented Jul 12, 2022

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

dsyme commented Jul 12, 2022

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

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

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

dsyme commented Jul 12, 2022

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

dsyme commented Jul 12, 2022

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

@smoothdeveloper
Copy link
Contributor Author

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

smoothdeveloper commented Jul 12, 2022

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

Related: #968, caveat: #968 (comment)

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

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

3 participants