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

Allow access to record type constructors in F# #722

Open
5 tasks done
charlesroddie opened this issue Feb 27, 2019 · 44 comments
Open
5 tasks done

Allow access to record type constructors in F# #722

charlesroddie opened this issue Feb 27, 2019 · 44 comments

Comments

@charlesroddie
Copy link

charlesroddie commented Feb 27, 2019

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:

  • Verbose syntax compared to C#, with all field names needing to be written out.
  • Poor intellisense support. Without the binding to r it would be hard to determine the type of the record. In order to enter the record, you have to know the field names, and intellisense does not assist.
  • Activation of an unreliable type-inference system which relies on guesswork: record type inference guesses types based on field names entered. This requires an annotation to solve, which is extra verbosity.
  • The type is not clear when reading code; to work out the type, the reader must mentally implement the algorithm in the compiler, which is more work than reading a single word.

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:

  • This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • This is not a breaking change to the F# language design
  • I or my company would be willing to help implement and/or test this
@charlesroddie charlesroddie changed the title Allow access to record type primary constructor in F# Allow access to record type constructors in F# Feb 27, 2019
@abelbraaksma
Copy link

abelbraaksma commented Feb 28, 2019

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

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

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

  • By being after a { we know we're inside of a record
  • Typing anything in between the brackets will filter completion based on record label names if it can

image

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

image

@charlesroddie
Copy link
Author

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

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

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

theprash commented Mar 4, 2019

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

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

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

charlesroddie commented Mar 8, 2019

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

cartermp commented Mar 8, 2019

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

dsyme commented Mar 9, 2019

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

@7sharp9
Copy link
Member

7sharp9 commented Mar 9, 2019 via email

@voronoipotato
Copy link

voronoipotato commented Mar 25, 2019

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

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

7sharp9 commented Jun 9, 2019 via email

@cartermp
Copy link
Member

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

@dsyme
Copy link
Collaborator

dsyme commented Jul 1, 2019

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

@gusty
Copy link

gusty commented Jul 5, 2019

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

@charlesroddie
Copy link
Author

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

gusty commented Jul 5, 2019

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

charlesroddie commented Jul 5, 2019

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

cartermp commented Jul 5, 2019

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

@charlesroddie
Copy link
Author

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

@gusty
Copy link

gusty commented Jul 6, 2019

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

7sharp9 commented Jul 6, 2019

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

gusty commented Jul 6, 2019

When you say normal constructor do you mean tupled ones?

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

@7sharp9
Copy link
Member

7sharp9 commented Jul 6, 2019

Yep tupled ones

Anything curried has no tooling support for completion params

@charlesroddie
Copy link
Author

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

gusty commented Jul 7, 2019

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

Better have curried and non curried.

@cartermp
Copy link
Member

cartermp commented Jul 7, 2019

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

gusty commented Jul 7, 2019

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

dsyme commented Jul 8, 2019

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

7sharp9 commented Jul 8, 2019

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

gusty commented Jul 9, 2019

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

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

@theprash
Copy link

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

7sharp9 commented Aug 19, 2019

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

giuliohome commented Nov 21, 2019

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

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

image
image

@giuliohome
Copy link

giuliohome commented Oct 3, 2022

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

baronfel commented Oct 3, 2022

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

@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

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