fsharp / fslang-suggestions

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

Allow access to record type constructors in F# #722

Open charlesroddie opened 5 years ago

charlesroddie commented 5 years ago

RFC FS-1073

F# has concise syntax for defining records:

type PensionData =
    { Name:string; ProbableNumberOfYearsUntilRetirement:int }

Creation of records in C# is easy, using the constructor:

PensionData r = PensionData("Adam",10)

This is convenient as to enter the data you can just type the class name, a bracket, and intellisense will tell you what the fields should be.

Creating records in F# is clunky, because the constructor is not accessible:

let r = { Name = "Adam"; ProbableNumberOfYearsUntilRetirement = 10 }

The disadvantages are:

Extra information

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

Related suggestions:

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

abelbraaksma commented 5 years ago

For a contrasting opinion (though I'm not against the proposal per se), I've just refactored hundred or so POCO's into records, they used C# class style in F#, so that I could use the verbose syntax. Simple reason? Readability. I went nuts over each time trying to find out what each argument in the constructor was, jumping back and forth to the definition.

After refactoring was done, code was easier to follow and reason about, and I felt sorry for all C# programmers (including myself) not having such clear syntax.

Also, the recently much improved inference doesn't give me any serious trouble.

That said, esp for records with one or two fields, I can see the benefit of having two ways of doing things.

davidglassborow commented 5 years ago

https://sharplab.io/#v2:DYLgZgzgNAJiDUAfALgTwA4FMAEAFTAThAPYB22AvNgN7YCCA5jiNgJanLYC+QA=

Doesn't look like the constructor is hidden by attribute or something, so not sure how F# ignores the constructor

cartermp commented 5 years ago

Just a note on IntelliSense, there are some things it helps with today:

image

But if it's not the first character the preselected item will be different:

image

charlesroddie commented 5 years ago

Is this actually a language suggestion? This is not explicitly prohibited in the language specification as far as I can see. If someone figures out what is preventing access to the constructor and submits a PR, will that be OK?

voronoipotato commented 5 years ago

I personally don't see anything wrong with constructing records with record(a,b), it's basically just a shorthand for a create function. Perhaps we could have the developer be able to write a create function that returns the record. I've thought about how this would be nice for DU's where we have some private DU to prevent the creation of it without our create function, however usually I want a create that returns an Option rather than a simple DU. The reason I want my type to be private is because I don't want to have other developers creating that DU without following the constraints I have on that DU.

davidglassborow commented 5 years ago

As a side note, rather than a private DU, I just go for the approach of rebinding the constructor, although I never see others do it, so I must be missing something

type IntLargerThanZero = IntLargerThanZero of value:int
let IntLargerThanZero x = if x > 0 then IntLargerThanZero x |> Some else None
theprash commented 5 years ago

To me, the existing verbose record construction fits in with the general F# theme of being more explicit than implicit where it can affect safety, as in the ability to refactor without changing behaviour. Currently the order of record fields has almost no effect on behaviour and it's safe to re-order them as a refactoring. But suppose you had a record with multiple fields of the same type:

type PensionData =
    { Name : string
      ProbableNumberOfYearsUntilRetirement : int
      YearsWorked : int }

If we decide to swap the position of the last two fields in the definition then we change the meaning of any usage of a constructor that relies on the order (e.g. PensionData("Name", 10, 30)), without creating any compiler errors.

I would consider this to be the biggest disadvantage because I feel that anything that can subtly break your runtime is an order of magnitude more inconvenient in the long run than typing some more identifiers.

voronoipotato commented 5 years ago

@davidglassborow well I mostly didn't do that because I didn't even know that you COULD do that. Given that you can just rebind the constructor, maybe if we have a default constructor it should simply be alphabetical. If you want a specific order for your constructor you can rebind them as @davidglassborow shows. This way we can shuffle the order of arguments in our traditional way of doing records as we please without breaking the build. If someone wants that specific order represented in the auto-constructor they can rebind it, and perhaps we can include that in documentation.

BentTranberg commented 5 years ago

I agree with @theprash, and I can also imagine situations where things can start to go wrong because the compiler is no longer as strict. And this will affect my coding even if I do not want to use this new feature. I question whether it is correct that this is not a breaking change to the F# language design. Let's say I want to change my class into a record. Then I have to watch out for any class constructors mistakenly being left as this new kind of record constructor, which is not what I want. I want the compiler to continue to alert me of problems in this case, just as it will do whenever I add or remove a case to a DU. Let's not spoil that.

Besides, I don't see the point. It's very easy to do this.

let pensionData name probableNumberOfYearsUntilRetirement =
    { Name = name; ProbableNumberOfYearsUntilRetirement = probableNumberOfYearsUntilRetirement }

let pd = pensionData "name" 150
charlesroddie commented 5 years ago

@theprash To me, the existing verbose record construction fits in with the general F# theme of being more explicit than implicit where it can affect safety

Debatable. The intellisense guesswork on records means that it often mistakes one record type for another which has the same or similar field names. Record-specific construction is explicit about fields but not explicit about the type. (NB classes can be explicit about fields too via named arguments.)

@BentTranberg I question whether it is correct that this is not a breaking change to the F# language design... this new kind of record constructor

This is not a breaking change. This is not debatable. No code currently compiling breaks when the constructor is unhidden. This constructor also is not new.

It's very easy to do this...

We have a static Create method defined on every record and it would be nice to get rid of these lines of code which are duplicating functionality that is currently exposed to all .Net code except F#.

cartermp commented 5 years ago

Also worth noting that the only sane way to construct records and ensure they work without issue with other lang constructs and refactorings is to apply even more verbosity: https://docs.microsoft.com/en-us/dotnet/fsharp/style-guide/formatting#formatting-records

dsyme commented 5 years ago

Just to note that one specific reason to allow this is that it allows using the record constructor as a first class value.

7sharp9 commented 5 years ago

Could we not just remove the warning, and tip/completion hiding that occur now?

On Sat, 9 Mar 2019 at 16:17, Don Syme notifications@github.com wrote:

Just to note that one specific reason to allow this is that it allows using the record constructor as a first class value.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/fsharp/fslang-suggestions/issues/722#issuecomment-471196925, or mute the thread https://github.com/notifications/unsubscribe-auth/AAj7ynTRQbWp3n9lXSbwOW0ooiqln6Lnks5vU962gaJpZM4bVR3U .

voronoipotato commented 5 years ago

This may end up becoming a separate but somewhat related RFC but OCaml has field and label punning for record construction. This means if you have a variable that is the same name as the record label you can just put it in there.

let create_host_info ~hostname ~os_name ~cpu_arch ~os_release =
    { os_name; cpu_arch; os_release;
      hostname = String.lowercase hostname;
      timestamp = Time.now () };;

versus

 let create_host_info
    ~hostname:hostname ~os_name:os_name
    ~cpu_arch:cpu_arch ~os_release:os_release =
    { os_name = os_name;
      cpu_arch = cpu_arch;
      os_release = os_release;
      hostname = String.lowercase hostname;
      timestamp = Time.now () };;

obviously the ~labels help with this specifically because they address @theprash 's concern directly. Is there any RFC for labeled arguments?

(see https://v1.realworldocaml.org/v1/en/html/records.html )

jannesiera commented 5 years ago

I would be very happy if this change would make it into the language. As @dsyme points out that would make the constructor as a first class value.

This is something I ran into writing generic UI components to 'lift' a record to a form UI component. The lifting function is completely generic and given simple components that return the correct field-level types constructs an instance of the record type and passes it to its parent component.

Since I am unable to access the constructor as a first-class value and the record syntax is not generic enough (since I don't care about the field names) I have to manually write the create function myself.

As mentioned here already this (1) is a lot of boilerplate and (2) clutters the code.

Related is the fact that I would like getters on record fields to be first-class values as well.

To illustrate my usecase, given a record:

type Person {
  firstName: string;
  lastName: string;
  age: int;
}

I have to write this boilerplate:

let cons firstName age: Person = { firstName = firstName; lastName = lastName; age = age; }
let firstName p = p.firstName
let lastName p = p.lastName
let age p = p.age

The problem with writing this boilerplate is that (1) whenever the record changes I have to manually change this code and (2) it interferes with F#'s ability to elgantly express the domain model in a way that a non-technical person can read/understand it.

7sharp9 commented 5 years ago

This is exact hat myriad generates.

On Sun, 9 Jun 2019 at 15:01, Janne Siera notifications@github.com wrote:

I would be very happy if this change would make it into the language. As @dsyme https://github.com/dsyme points out that would make the constructor as a first class value.

This is something I ran into writing generic UI components to 'lift' a record to a form UI component. The lifting function is completely generic and given simple components that return the correct field-level types constructs an instance of the record type and passes it to its parent component.

Since I am unable to access the constructor as a first-class value and the record syntax is not generic enough (since I don't care about the field names) I have to manually write the create function myself.

As mentioned here already this (1) is a lot of boilerplate and (2) clutters the code.

Related is the fact that I would like getters on record fields to be first-class values as well.

To illustrate my usecase, given a record:

type Person { firstName: string; lastName: string; age: int; }

I have to write this boilerplate:

let cons firstName age: Person = { firstName = firstName; lastName = lastName; age = age; } let firstName p = p.firstName let lastName p = p.lastName let age p = p.age

The problem with writing this boilerplate is that (1) whenever the record changes I have to manually change this code and (2) it interferes with F#'s ability to elgantly express the domain model in a way that a non-technical person can read/understand it.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/fsharp/fslang-suggestions/issues/722?email_source=notifications&email_token=AAEPXSTHHE5XE2M63WARZFTPZUELVA5CNFSM4G2VDXKKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODXIKVRY#issuecomment-500214471, or mute the thread https://github.com/notifications/unsubscribe-auth/AAEPXSVKOV4Z5JUZGI2QBZDPZUELVANCNFSM4G2VDXKA .

cartermp commented 5 years ago

Generally I think I'm in favor of doing this. @dsyme thoughts?

dsyme commented 5 years ago

@cartermp Yes, I am in favour of this. Marking as approved

gusty commented 5 years ago

Now the tricky question is, should the constructor use curried arguments or not? I would prefer curried ones.

charlesroddie commented 5 years ago

@gusty The constructor already exists and uses uncurried arguments. (And is also preferable as there is no information that a specific order of partial application will be useful.)

gusty commented 5 years ago

And is also preferable as there is no information that a specific order of partial application will be useful.

Could you expand on this? Right now I'm failing to see advantages of non-curried constructors. You mention specific order, but specific order would be there in both cases.

charlesroddie commented 5 years ago

A(a:int,b:float) is a function which takes an int*float tuple and returns an A. A (a:int) (b:float) is a function which when given an int, returns a function which when given an float returns an A. So the curried form is more complex.

The reason for paying the cost of greater complexity is that you may want the partially applied function A a, which using the uncurried constructor would be fun b -> A(a,b). The type signature int->float->A implies that you have this in mind.

The other partially applied function fun a -> A a b is no easier to write with the curried constructor. So choosing a curried form is best when you expect that partial application is useful, and you know the probable order of partial application.

For record type constructors neither condition is satisfied.

cartermp commented 5 years ago

@charlesroddie Would you like to take a stab at an RFC for this?

charlesroddie commented 5 years ago

Done. I thought it might be too straightforward for an RFC but then I saw "Wildcard self identifiers".

gusty commented 5 years ago

@charlesroddie By reading your explanation my interpretation (correct me please if I'm wrong) is that, there's no advantage for non-curried form, and for the curried one there is a slight advantage only in the case the partial applied function you want it's in the right order.

Now, let me point out what I consider a very important advantage: by having a curried function you can apply any applicative effect to it. There are many examples and many uses but the typical one is validation:

let person = Person <!> name <*> lastname
> val person: person option

Many libraries makes heavy use of this style, FParsec, Fleece and most json libraries to name just a few. And the lack of this makes this code (fun a b -> new Person(a, b) populate up to 25% of the code, adding un-necessary noise.

We have currying in F# and it's standard to prefer curried function over tupled ones in normal F# writing style, this is because in FP currying is a very useful abstraction that allows to compose code in different ways and F# is FP first.

Having said this, I would be interested in knowing a specific scenario where a tupled constructor presents a real advantage. My intuition is that tupled constructors force you to construct the record all at once, which is the same as the standard record construction we have now, but having to remember the order, which makes preferable the latter.

7sharp9 commented 5 years ago

When you use a constructor though you get tooling support telling you all the parameters it requires. I think having normal constructors would be a better experience.

gusty commented 5 years ago

When you say normal constructor do you mean tupled ones?

Don’t you get tooling support with curried functions as well?

7sharp9 commented 5 years ago

Yep tupled ones

Anything curried has no tooling support for completion params

charlesroddie commented 5 years ago

@gusty By reading your explanation my interpretation (correct me please if I'm wrong) is that, there's no advantage for non-curried form

Sorry you did read wrong. I said the non-curried one is simpler. There is hardly a more important advantage in software.

by having a curried function you can apply any applicative effect to it

This example does seem to be a valid advantage for curried functions for a certain style of programming. You can't define a function that takes an n-tuple and returns an (n+1)-tuple so you can't do exactly the same with non-curried functions. An alternative style is computation expressions, which would work whether or not the function is curried.

gusty commented 5 years ago

@7sharp9 Fair point, which applies by extension to all non-curried functions and makes me wonder if we should move the focus to tooling improvement, given that most F# functions are curried.

@charlesroddie Sorry if I did read wrong, I read it many times and I still can't figure out what's the simplicity advantage.

Again, correct me if I'm wrong, you say that: int->float is more complex than int*float ?

I need more information, complex in which sense? For instance, I agree in that the internals are more complex for .NET languages but at the user level, do you really think there is an additional complexity in a language where almost all functions of its main library (FSharp.Core) are in a curried form?

What you mention about the computation expression is already possible with the record construction syntax. Also, and this may raise a different discussion, applicative style is very compact, computation expressions defeat that purpose, the current efforts to incorporate it to CEs are driven by efficiency, but not to improve syntax, actually it will look more or less the same as it is now IMHO.

realvictorprm commented 5 years ago

Better have curried and non curried.

cartermp commented 5 years ago

@gusty I don't think it's possible to allow for curried constructors without broadening this to a different issue. Since the record constructor is a constructor, as of F# 4.0 you can use it as a function (i.e., pipe into it) provided the types match up, just like with classes. But there's no way to do this today:

type C(x: int, y: int) =
    member __.Foo = x

let curried = 12 |> C

So either:

(a) A different construct has to be created to allow for curried creation of records, or (b) Constructors in F# are revisited to allow for currying in some form

Neither option is the same as allowing access to the existing constructor that we generate.

gusty commented 5 years ago

I see, I was thinking about providing a curried constructor, so something along option (a). Otherwise, what we gain here is not that interesting. Although there might be cases where a tupled constructor is preferable to a curried one, it's just that I can't think of such scenario at the moment.

dsyme commented 5 years ago

Just to say we would not have curried constructors. It's partly because of

  1. the tooling issue mentioned by @7sharp9
  2. existing class constructors are non-curried
  3. the feature gels nicely with named arguments and curried forms don't support named arguments
  4. this feature makes it more reasonable to support optional fields in records, since they then map to optional named arguments

In essence, like class member application, non-curried application is "a point of rich names, metadata and auto-conversion" in the F# design.

Curried application is "incidental, cheap and metadata free" (few or no names, no attributes, no optionality etc.).

Since record declarations are by their nature metadata-rich, they fit more with the set of features supported by the former than the latter.

7sharp9 commented 5 years ago

@gusty Maybe add a suggestions to first improve curried tooling?

This has been discussed before but there was no consensus on an efficient way of implementing it. It would probably require an arbitrary check on the partially applied functions type that would have to be triggered on a ctrl+cpace or just space if you were in the middle of applying characters, as opposed to prompting on entering a comma and changing the parameter display.

gusty commented 5 years ago

Sorry for the silent, but after reading all your comments realized that a curried constructor will never make it to F#, so I was busy trying to figure out a generic polyvariadic curry function that would serve the purpose. I'm about to send a PR to F#+ for these functions.

It works nicely for constructors as well, as long as they don't have overloads:

type C(x: int, y: int) =
    member __.Foo = x

let curried = 12 |> curry C

so all I will ask is please don't generate overloads for the constructor.

realvictorprm commented 5 years ago

A curried version of the record constructor would be great, sad we wont have any.

theprash commented 5 years ago

One advantage of the existing behaviour, where the field name must be used, is that if you want to find every place in your code where the field was assigned a value you can do find all usages on the field name. I take advantage of this quite a lot.

If this constructor was accessible then you would have to also find usages on the constructor and count arguments to check what value was assigned.

Personally, in the interest of keeping larger codebases more maintainable I would add an item to my internal F# style guide to avoid this feature.

7sharp9 commented 5 years ago

I find that in larger codebases you start to outgrow records and switch to nominal types with constructors anyway.

Personally I would prefer constructor syntax as I find finding usages from record construction syntax to be really hit and miss, records don't tend to scale that well in a larger codebase.

giuliohome commented 4 years ago

Don't hurry up too much. ;-) As of today, we can already do

let recordFields record = 
    FSharpType.GetRecordFields(record.GetType()) 
    |> Seq.map (fun field -> (field.Name, FSharpValue.GetRecordField(record,field))) 
    |> Map.ofSeq

(a complete example in this snippet)

and ... lol ... for F# transpiled to JavaScript we don't even need reflection.

lucasteles commented 1 year ago

I would love to have the tooling show only valid fields on record completion when the typed is known

image image

giuliohome commented 1 year ago

@lucasteles I'm afraid this has to do with Visual Studio Code as an IDE (as opposed to Visual Studio 2022, e.g.), not with F# as a language compilator etc... Sorry if I'm misunderstanding you (but I guess you should rather look at VS Code Extensions for F# ...)

baronfel commented 1 year ago

@giuliohome you're correct - @lucasteles please open this as an issue at https://github.com/fsharp/fsautocomplete if you'd like to see this change in VSCode.

lucasteles commented 1 year ago

@lucasteles I'm afraid this has to do with Visual Studio Code as an IDE (as opposed to Visual Studio 2022, e.g.), not with F# as a language compilator etc... Sorry if I'm misunderstanding you (but I guess you should rather look at VS Code Extensions for F# ...)

Yes you are right, I will open an issue there, just commented here because it kinda align with the subject