-
Notifications
You must be signed in to change notification settings - Fork 832
-
Note: I'm just sharing an idea, I don't know if it's possible to be implemented or if it's worth the difficulties of implementation.
Extending Records
When a user wants to create an order the client app sends this (simplified) form model:
type CartItem = { ProductId: int Quantity: int } type CreateOrderFormModel = { CurrencyCode: string UsePoints: bool UseWallet: bool DiscountCode: string option ClearCart: bool Items: CartItem list }
When an admin operator wants to create an order for a customer the admin panel client sends this (simplified) form model:
type AdminCartItem = { ProductId: int Quantity: int PriceOverwrite: decimal option } type CreateAdminOrderFormModel = { UserId: string CurrencyCode: string UsePoints: bool UseWallet: bool Items: AdminCartItem list }
Unfortunately, there is no way to extend/inherit a base record type without going to the OOP side of the F#, ideally something like this would be nice:
type CartItem = { ProductId: int Quantity: int } abstract type BaseOrderFormModel = { CurrencyCode: string UsePoints: bool UseWallet: bool Items: CartItem list } type CreateOrderFormModel extend BaseOrderFormModel = { DiscountCode: string option ClearCart: bool } type AdminCartItem extend CartItem = { PriceOverwrite: decimal option } type CreateAdminOrderFormModel extend BaseOrderFormModel = { UserId: string Items: AdminCartItem list }
The extend keyword would just copy the base record fields, and we would still get two separate sealed classes. Any field of the base record can be overwritten by simply reusing its name and assigning a different type to it.
Type Pattern
Consider the following valid F# code:
type T0 = { Name : string } type T1 = { Name : string Age : int } type T2 = { Name : string Location : string } let inline getName(a : 'T when 'T : (member Name : string)) = a.Name let a = { Name = "a" } let b = { Name = "b"; Age = 22 } let c = { Name = "c"; Location = "home" } printfn $"Name: {getName a}" printfn $"Name: {getName b}" printfn $"Name: {getName c}"
If we want a function that would extract the Name out of any type that has a Name field, we would write the getName function, which is very ugly to look at.
It would be great if we could do this for example:
let getName(a :##{| Name: string |}) = a.Name
which says give me any type (named or anonymous) that has a Name field with the type string.
Or this:
let getName(a :##T0) = a.Name
which says give me any type (named or anonymous) that has all of the fields of the type T0, and possibly more without necessarily being inherited from it.
In that case when I want to write a function that would validate the currency code, I could write this:
let validateCurrency(model:##{| CurrencyCode: string |}) : Result<string, string> = // ...
and if I want to validate order items, I could write this:
let validateOrderItems(model:##{| Items:##CartItem |}) : Result<string, string> = // ...
and if I want to validate the UseWallet for a customer, I know it must be compatible with BaseOrderFormModel:
let validateUserWallet(model:##BaseOrderFormModel) : Result<string, string> = // ...
and if I want to validate a customer for admin order creation, I know it must be a CreateAdminOrderFormModel:
let validateOrderCustomer(model: CreateAdminOrderFormModel) : Result<string, string> = // ...
and if I want to validate a discount code, I know it must be a CreateOrderFormModel:
let validateDiscountCode(model: CreateOrderFormModel) : Result<string, string> = // ...
and so on.
Finally, if I want to group a bunch of validation functions together and pass them to a workflow (which would then try to get the first function that would return an error), maybe I could do something like this:
let orderValidations: (CreateOrderFormModel -> Result<string, string>) seq = seq { validateCurrency validateOrderItems validateUserWallet validateOrderDiscountCode // ... } let adminOrderValidations: (CreateAdminOrderFormModel -> Result<string, string>) seq = seq { validateCurrency validateOrderItems validateUserWallet validateOrderCustomer // ... }
The good news is that the validateCurrency function is not tied to any specific model so anywhere in the app that I need to validate a currency I can use this function (meaning I can include this function in any validation group as long as the model has a CurrencyCode field of string). The validateUserWallet requires a ##BaseOrderFormModel so I can happily reuse it in two different workflows with two different models (because both models extend that base type). The same goes for validateOrderItems. And finally, the validateOrderDiscountCode and validateOrderCustomer can only be included in validation groups that have their specific required models.
As you can see the 2 ideas are somewhat related to each other. I know that currently having these features are (mostly) possible if we go to the OOP side of F# (with heavy use of object inheritance) because I have done a similar approach in C#, but it would be nice if we didn't have to (going to that side kind of defeats the purpose of using F# in the first place).
Beta Was this translation helpful? Give feedback.
All reactions
Replies: 2 comments 6 replies
-
Maybe https://github.com/fsharp/fslang-suggestions/discussions would be a better place for this discussion?
Anyway, I think that fsharp/fslang-suggestions#1253 would address your first "extending records" suggestion, although it wouldn't be through inheritance.
Your second suggestion looks like fsharp/fslang-suggestions#11, which has been declined.
Beta Was this translation helpful? Give feedback.
All reactions
-
I think the issue 1253 is for record instantiation, my suggestion is for record type definition.
fsharp/fslang-suggestions#1253 (comment) does mention types and gives an example with records that is exactly what you are suggesting:
Types?
How about type syntax? e.g.
type A = { X: int Y: int } type B = { Z: int } type C = { ...A; ...B; Extra: string } let a = { X = 1; Y = 2 } let b = { Z = 3 } let c = { ...a; ...b; Extra = "four" }Spreading record types into other record types seems tempting, but note this would not give rise to subtyping.
As for your second suggestion: yes, I understand. That kind of "duck typing" is exactly what is meant by row polymorphism in fsharp/fslang-suggestions#11.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
I'm new to F# (❤️), I hope I'm making sense.
Welcome :)
Maybe we should add some kind of pinned post under discussions here to make it easier for people to find the language suggestions repo for language-suggestion-related things.
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
@brianrourkeboll it's possible to make an issue template that just points to an entirely different repo - checkout the new issue list in dotnet/sdk/issues for an example of what that can look like
Beta Was this translation helpful? Give feedback.
All reactions
-
👍 1
-
Yeah, this repo already has that for issues:
fsharp/.github/ISSUE_TEMPLATE/config.yml
Lines 6 to 8 in 841ba8e
It just doesn't have anything obvious under https://github.com/dotnet/fsharp/discussions or https://github.com/dotnet/fsharp/discussions/new/choose.
Beta Was this translation helpful? Give feedback.
All reactions
-
...You can create discussion category form templates (https://github.com/orgs/community/discussions/2838), but it doesn't look like you can easily redirect with a link like you can with issue templates.
Beta Was this translation helpful? Give feedback.
All reactions
-
Yes, this should go to suggestions repo
Beta Was this translation helpful? Give feedback.