fsharp / fsharp/fslang-suggestions
Infer record names from types
- Dominant language
- No language data
- Stars
- 373
- Forks
- 21
- PR merge metrics
- No merged PRs in 30d
Description
# Infer record names from types
F# is a great language for domain-driven design with single-case unions, DUs for "or" types, and records for "and" types. Some may even say that F# files containing domain types only can be read by non-programmers easily!
(Extracted from https://github.com/matthewcrews/ddd-with-fsharp/blob/master/Examples/Domain4.fsx)
```fs
type ProfitCategory =
| Cat1
| Cat2
| Cat3
type ItemQuantity = ItemQuantity of float
type InventoryId = InventoryId of string
type UnitCost = UnitCost of decimal
type SalesRate = SalesRate of float
type StockItem = {
InventoryId : InventoryId
UnitCost : UnitCost
SalesRate : SalesRate
ProfitCategory : ProfitCategory
}
type DaysOfInventory = DaysOfInventory of float
```
But there are lots of duplication that hinders this objective! Wouldn't it be nice if we can write:
```fs
type ProfitCategory = Cat1 | Cat2 | Cat3
type ItemQuantity of float
type InventoryId of string
type UnitCost of decimal
type SalesRate of float
type StockItem = { InventoryId; UnitCost; SalesRate; ProfitCategory }
type DaysOfInventory of float
```
In essence, we reduced
```fs
type DomainType1 = DomainType1 of WrappedType1
type DomainType2 = DomainType2 of WrappedType2
type DomainType3 = DomainType3 of WrappedType3
type OrType = DomainType1 of DomainType1 | DomainType2 of DomainType2 | DomainType3 of DomainType3
type AndType = { DomainType1 : DomainType1; DomainType2 : DomainType2; DomainType3 : DomainType3 }
```
to
```fs
type DomainType1 of WrappedType1
type DomainType2 of WrappedType2
type DomainType3 of WrappedType3
type OrType = (DomainType1 | DomainType2 | DomainType3)
type AndType = { DomainType1; DomainType2; DomainType3 }
```
. The deduplication for single-case unions belong to #727, the deduplication for "or" types belong to #538, and here I'll propose the deduplication of "and" types.
A record or anonymous record written as
```fs
type Record = { Type1; Type2; Type3 }
```
will be interpreted as
```fs
type Record = { Type1 : Type1; Type2 : Type2; Type3 : Type3 }
```
where the constituent names are interpreted as types, highlighted as types, and the names will be inferred from the types.
When one of the types is a generic specialization, the name in source will be used:
```fs
type Shelf = { List; ShelfCategory }
type Warehouse = { Shelf list }
```
```fs
type Shelf = { ``List`` : List; ShelfCategory }
type Warehouse = { ``Shelf list`` : Shelf list }
```
However, having generic type parameters inside a type without a corresponding field label is not allowed.
```fs
type Shelf<'T> = { List<'T>; ShelfCategory } // Not allowed!
type Shelf<'T> = { Items : List<'T>; ShelfCategory } // Must be used
```
The alternative is to infer a name of `List<'T>` which is inconsistent with the type when specialized.
When dot-access notation is used, only the last part is used as the name.
```fs
type OrderTaken = { OrderTaking.Domain.Order; OrderTaking.Domain.OrderTaker }
```
```fs
type OrderTaken = { Order : OrderTaking.Domain.Order; OrderTaker : OrderTaking.Domain.OrderTaker }
```
When two names collide, error.
```fs
type InvalidDomainType = { OrderTaking.Domain.Order; Shipping.Domain.Order } // Error: Duplicate name "Order"
```
The duplicate name error also applies to:
```fs
type ``Shelf list`` = unit
type Warehouse = { Shelf list; ``Shelf list`` }
```
## Pros and Cons
The advantages of making this adjustment to F# are
1. Conciseness
2. Readability to non-programmers like business analysts
3. Correctness w.r.t. domain design without duplicate names like in current code
The disadvantage of making this adjustment to F# is that this introduces additional rules and syntax to learn. However, #653 is approved and has the same disadvantageous properties but for record construction.
## Extra information
Estimated cost (XS, S, M, L, XL, XXL): M
Related suggestions:
#727 and #538 - Deduplication for single-case unions and "or" types
#653 - Infer record labels from expressions
This proposal has an analogy for record creation as shown in that proposal. However, it uses name inference via `nameof`, but `nameof(seq)` produces `seq` which would not be desirable here. Even with `nameof(int seq)` being an error currently, the only consistent output for it would be also `seq` as shown in #953. Moreover, it cannot purely rely on `nameof` name lookup either, because it wouldn't [make sense to have local bindings starting with uppercase letters just to infer names](https://github.com/fsharp/fslang-suggestions/issues/653#issuecomment-531357524), therefore
```fs
let a = 1
let b = {| B = 2 |}
{| a; b.B |}
```
would result in a value of type `{| A : int; B : int |}`, which makes it more complex than just a `nameof` lookup, like seen in this proposal.
#600 - Intersection types
Intersection types provide an alternate way to solve this problem. However, they not only take a huge implementation effort, but also:
- Rely on downcasts for element extraction, so IntelliSense can't help with it, compared to property extraction using the dot-notation with records.
- Fit into parameters taking one of its constituent types via implicit downcasting with #849. This means that a domain AND type can magically fit into a constituent domain type. This is quite a jump in logic especially if the AND type is aliased as frequently done with normal records.
- Will not have effective interoperability with C#. Granted deduplicating "or" types also do this, but the loss of type safety is small compared to having intersection types without C# cooperation.
- Include all exposed members and extension members of their constituent types. "And" types are different from their constituent elements! They represent different things in the domain. Mixing members like this not only conflates domain objects, but also produces IntelliSense pollution.
#747 - Infer record field types from names
That proposal which bears syntactical resemblance to this proposal was shot down quickly after being posted. This proposal is different from it because:
- That is about making presumably new single-case DUs magically just by specifying names in a record. That is too implicit with type declarations without the `type` keyword, and conflates name deduplication with primitive type avoidance.
- Instead of being symmetric with #653 where names are inferred from types, new types are inferred from names. This proposal infers names from types instead, being more coherent with #653.
Therefore, that proposal cannot be used to justify rejecting this proposal.
## 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
## For Readers
If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start by reading the proposal's examples and rules for record and anonymous-record field inference, then review the related suggestions #727, #538, and #653. Done means the language design is resolved for simple types, generic specializations, dotted names, collisions, and generic parameters, with implementation and tests identified.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- fsharp
- Domain
- compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 28/100