-
Notifications
You must be signed in to change notification settings - Fork 21
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 easier access to properties common to all cases in Discriminated Union #564
Comments
I'm against this because is breaks code on surprising ways when adding another case :) type ShoppingCart =
| EmptyCart of AppliedDiscountCode: string
| CartWithItems of AppliedDiscountCode: string * Items: string list
| CompletedCart of AppliedDiscountCode: string * Items: string list * CalculatedTotal: decimal
member x.DiscountCode = x |> function
| EmptyCart (AppliedDiscountCode=discount) | CartWithItems(AppliedDiscountCode=discount)
| CompletedCart(AppliedDiscountCode=discount) -> discount which makes this a bit better. The only way I see this feature is when it's only possible to access it from within the DU definition (to limit the number of surprising code locations where stuff breaks). But that would mean special syntax or special rules within the DU (which again is bad)... Also if stuff is part of every case you might want to consider to re-design it to type ShoppingCartList =
| EmptyCart
| CartWithItems of Items: string list
| CompletedCart of Items: string list * CalculatedTotal: decimal
type ShoppingCart =
{ AppliedDiscountCode: string; CartList: ShoppingCartList } if you properly hide stuff behind modules this works surprisingly well, and often improves the code as well. |
An alternative is to allow union types to also have a common set of fields: type ShoppingCart =
val DiscountCode: string
| EmptyCart
| CartWithItems of Items: string list
| CompletedCart of Items: string list * CalculatedTotal: decimal There's no real underlying technical reason we can't allow something this, though it's a bit odd. Is it a record type or a union type? I'd be interested in Scott Wlaschin's take on this for modelling. The construction syntax is a challenge, probably would need to use a named arg e.g. |
@forki I note that some Elmish.Fable view models would definitely benefit from this, e.g. type Model =
val User : UserData option
| HomePageModel
| LoginModel of Login.Model
| WishListModel of WishList.Model instead of the two types: type PageModel =
| HomePageModel
| LoginModel of Login.Model
| WishListModel of WishList.Model
type Model =
{ User : UserData option
PageModel : PageModel } It's nice that one type has been saved - it makes the model more obvious, and this feels like a common pattern for UI view models that compose a set of possible views and add some global information relevant to all the views. However it's not obvious what copy-and-update would be for such a type when updating only the union portion, e.g. what is the equivalent of this? { model with
PageModel = WishListModel m } e.g. something new like
|
I'm not sold on this being a good idea. I agree that it can sometimes be annoying or inconvenient to have to define a DU and a Record separately, but that separation also forces you to keep things a bit cleaner. In @dsyme's example this is convenient for a small Fable app, but a |
I really like the idea of base fields for union types. This would save a lot of extra objects and/or duplication we have going on right now. Struggling to find any real downsides because you can just not use the feature if you're not a fan. It almost points to something a bit richer, allowing multiple levels with certain things shared at each level. |
Interesting idea, but how would the data access look like?
Tbh the type declaration is not really that annoying. Does it help in the
init, update and view?
|
I would very much like to be able to access common properties within a union type. See this example (Extracting parsed json provider payloads to a common type): namespace CryptoApi.Data
open Rationals
open System
type public Order = {
Id: Guid;
Market: Market;
State: string;
Side: string;
Type: string;
Price: Rational;
Size: Rational;
Filled: Rational;
Timestamp: int64;
}
////////////////////////////////////////////////
namespace CryptoApi.Exchanges.Cobinhood.Data.Providers
open FSharp.Data
module Trading =
type GetOrderResponse = JsonProvider<"""{
"success": true,
"result": {
"order": {
"id": "37f550a202aa6a3fe120f420637c894c",
"trading_pair": "BTC-USDT",
"state": "open",
"side": "bid",
"type": "limit",
"price": "5000.01",
"size": "1.0100",
"filled": "0.59",
"timestamp": 1504459805123
}
}
}""">
type GetAllOrdersResponse = JsonProvider<"""{
"success": true,
"result": {
"orders": [
{
"id": "37f550a202aa6a3fe120f420637c894c",
"trading_pair": "BTC-USDT",
"state": "open",
"side": "bid",
"type": "limit",
"price": "5000.01",
"size": "1.0100",
"filled": "0.59",
"timestamp": 1504459805123,
"eq_price": "5000.01"
}
]
}
}""">
////////////////////////////////////////////////
namespace CryptoApi.Exchanges.Cobinhood.Data.Transformers
open CryptoApi.Data
open CryptoApi.Exchanges.Cobinhood.Data.Providers.Trading
module Orders =
open Rationals
type TypeOfOrderResponse =
| OrdersOrder of GetAllOrdersResponse.Order
| OrderOrder of GetOrderResponse.Order
let ExtractOrderFromPayload (knownMarkets: Market[]) (payload: TypeOfOrderResponse): Order =
let symbolFromOrder = match payload with
| OrdersOrder o -> o.TradingPair
| OrderOrder o -> o.TradingPair
let marketEqualsSymbol = fun km -> km.symbol.Equals(symbolFromOrder)
let market = knownMarkets |> Array.find marketEqualsSymbol
let order = {
Market = market;
Id = match payload with
| OrdersOrder o -> o.Id
| OrderOrder o -> o.Id;
State = match payload with
| OrdersOrder o -> o.State
| OrderOrder o -> o.State;
Side = match payload with
| OrdersOrder o -> o.Side
| OrderOrder o -> o.Side;
Type = match payload with
| OrdersOrder o -> o.Type
| OrderOrder o -> o.Type;
Price = match payload with
| OrdersOrder o -> Rational.Approximate(o.Price)
| OrderOrder o -> Rational.Approximate(o.Price);
Size = match payload with
| OrdersOrder o -> Rational.Approximate(o.Size)
| OrderOrder o -> Rational.Approximate(o.Size);
Filled = match payload with
| OrdersOrder o -> Rational.Approximate(o.Filled)
| OrderOrder o -> Rational.Approximate(o.Filled);
Timestamp = match payload with
| OrdersOrder o -> o.Timestamp
| OrderOrder o -> o.Timestamp;
}
order
let ExtractOrder (knownMarkets: Market[]) (payload: GetOrderResponse.Root): Order =
(OrderOrder payload.Result.Order)
|> ExtractOrderFromPayload knownMarkets
let ExtractOrders (knownMarkets: Market[]) (payload: GetAllOrdersResponse.Root): Order[] =
payload.Result.Orders
|> Array.map (fun o -> ExtractOrderFromPayload knownMarkets (OrdersOrder o))
If there is a more concise technique for writing |
I encounter similar patterns in my code, but it is rare that it is simple field access among the DU cases, it generally involves additional logic, so I'll likely not use this frequently, at least in my current approach. I'm not against it, but also concur that it might be a design smell, why not have a record containing the shared field along with an instance of DU? type ShoppingCartStatus =
| EmptyCart
| CartWithItems of Items: string list
| CompletedCart of Items: string list * CalculatedTotal: decimal
type ShoppingCart = { AppliedDiscountCode: string; Status: ShoppingCartStatus } Going with the idea, it might be relevant to have ability to expose an option value for fields that aren't defined for all cases (it would be exposed if a field is present in more than a single case). type ShoppingCart =
| EmptyCart of AppliedDiscountCode: string
| CartWithItems of AppliedDiscountCode: string * Items: string list
| CompletedCart of AppliedDiscountCode: string * Items: string list * CalculatedTotal: decimal This would have an |
I think my case may be more specific to JsonProviders where multiple providers can contain similar payloads |
I'd like to poke this thread, since I've just run into this case myself. My thoughts on previous comments:
I don't see what is surprising. Pattern matching code already breaks when adding a new case (and rightly so).
Personally, I think the originally proposed syntax is preferable to @dsyme's proposal, because it doesn't interfere with positional construction or deconstruction, and is a less invasive change to the language. Ultimately, I'd like to advocate for this change, since I think it helps streamline and remove boilerplate in some cases, with relatively no drawback. |
I like this version instead. If you add a new case with fields no longer common, its should be removed as a member? |
How would the creation of such type look like? Although Scala 3 has enum which is close to DU in F#, users still use sealed traits which models this kind of data. |
I propose we add property accessors for all properties that are common among all cases in a discriminated union at the type level. Both the Label and the Type of the property can be used to determine the set of common properties.
For example:
AppliedDiscountCode is common among all cases, and if I had a variable
v
of typeShoppingCart
, I must be able to dov.AppliedDiscount
.The existing way of approaching this problem in F# is:
Pros and Cons
The advantages of making this adjustment to F# are:
The disadvantages of making this adjustment to F# are:
Extra information
Estimated cost (XS, S, M, L, XL, XXL): XS
Affadavit (must be submitted)
Please tick this by placing a cross in the box:
Please tick all that apply:
The text was updated successfully, but these errors were encountered: