fsharp / fsharp/fslang-suggestions

Allow records/DUs to have different visibility constructors to their fields

Open
#852 24 comments 12 reactions 0 assignees View on GitHub
approved-in-principle area: records area: unions needs rfc
Dominant language
No language data
Stars
373
Forks
21
PR merge metrics
No merged PRs in 30d

Description

I propose we opt-in to restricting construction of records/DUs to a given scope (via private/internal).

When you encounter the problem of trying to maintain an invariant of a type (which cannot be expressed in the type system) you often resort to writing smart/safe constructors.

In F#, for records and DUs, this sadly results in hiding the entire internals of the type. This is because the constructor cannot be separated from the rest of the internals in terms of visibility. Given how desirable it is to use records and DUs in F# (because of the syntactic sugar afforded to them), it is sad we can't use them as intended.

The best we have currently is to re-expose the fields through members as shown:
```fsharp
type SafeFilePath =
internal
{
DirectoryName_ : string
FileName_ : string
}
static member Create (path : string) : Result =
failwith "Insert parsing/error checking logic here".

member this.DirectoryName : int = this.DirectoryName_
member this.FileName : string = this.FileName_
```
But this approach has multiple downsides:
- You lose the ability to pattern match on the record.
- You lose the ability to derive an anonymous record from your record.
- You have boilerplate in the definition of the type.
- Consumers now have a method call to retrieve a field from the record, instead of a field load. This has performance implications.

Similarly with Discriminated Unions:
```fsharp
type MyDU =
internal
| X_ of int
| Y_ of string

[]
module MyDU =
let (|X|Y|) = function | X_ i -> X i | Y_ i -> Y i
```
This is more usable from the perspective of a consumer, but it also has downsides:
- Active patterns compile down to allocating Choice objects, and are therefore much less performant than regular pattern matching.
- It's boilerplate.

I propose we could replace this with:
```fsharp
type MyRecord internal () =
{
X : int
Y : string
}
```
or
```fsharp
type MyRecord internal new =
{
X : int
Y : string
}
```
or
```fsharp
type MyRecord =
{
X : int
Y : string
}
internal new
```
The syntax for DUs would be identical, and would apply the visibility to all cases.

## Pros and Cons

The advantages of making this adjustment to F# are:
- Less boilerplate
- Better performance
- Nicer syntactic sugar (for records mainly)

The disadvantages of making this adjustment to F# are:
- More syntax

## Extra information

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

## Affidavit (please submit!)

Please tick this by placing a cross in the box:
* [x] This is not a question (e.g. like one you might ask on [stackoverflow](http://stackoverflow.com)) and I have searched stackoverflow for discussions of this issue
* [x] I have [searched both open and closed suggestions on this site](http://github.com/fsharp/fslang-suggestions/issues) and believe this is not a duplicate
* [x] 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:
* [x] This is not a breaking change to the F# language design
* [x] I or my company would be willing to help implement and/or test this

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.