3 ms·
The F# is certainly nice, but the C# is odd. This is a textbook example of either inheritance or interfaces. I did inheritance here but it would be trivial to c
by jimmaswell 3y ago
The F# is certainly nice, but the C# is odd. This is a textbook example of either inheritance or interfaces. I did inheritance here but it would be trivial to change it to an interface.
https://dotnetfiddle.net/F5fPZy https://dotnetfiddle.net/F5fPZy
The class implementations add a bit of upfront verbosity but the calculation at the end is arguably nicer to read: that method is concerned with applying the discounts, but in the F# case it has to know how to apply each type of discount, while the C# encapsulates that in the Discount implementation.
decimal subtotal = OrderLines.Sum(ol => ol.Product.Price \* ol.Quantity);
return Discount?.Apply(subtotal) ?? subtotal;
vs
let subtotal = List.sumBy (fun ol -> ol.Product.Price \* (decimal ol.Quantity)) orderLines
let totalDiscount =
discount
|> Option.map (fun d ->
match d with
| Percentage p -> subtotal \* (decimal p / 100M)
| FixedAmount f -> f)
|> Option.defaultValue 0M
subtotal - totalDiscount
Though I'm also pretty sure F# could do the same thing with the classes if you wanted. And someone else pointed out you can write some C# that looks the same as the F# if you use a switch statement. And I'm pretty sure you could make it look even more F# if you used more Linq.
I guess the F# matching is pretty much like interface/class checking as follows? I don't see the functional difference:
if (discount is IPercetage percentage) { .. subtotal \* percentage.value / 100 ...
else if (discount is IFixed fixed) { .. subtotal - fixed.value ...
And you could do even more with some newer C# features like anonymous classes etc.
I'm not left feeling like I'm missing out on anything without discriminated unions in particular. I'd be interested to see some cases where the functional nature of F# does make a large improvement over the equivalent C# though.
- jackmott 3y ago[dead]
- Smaug123 3y agoThe main difference is the totality checking. A discriminated union guarantees that you've handled the entire modelled domain (and nothing else); repeated interface checking merely guarantees that you've handled a subset of it (and also you may have handled a bunch of other stuff). It's so comforting to know up front that you've done everything you need to do!
- gsuuon 3y agoYou can also hide the logic away in F#: let calcDiscount = function | Percentage p -> subtotal * (decimal p / 100M) | FixedAmount f -> f And then: let totalDiscount = discount |> Option.map calcDiscount |> Option.defaultValue 0M F# is missing some of the convenience operators, but you could easily add these yourself if you wanted: let (?) x f = Option.map f x let (|?) x y = Option.defaultValue y x and so totalDiscount becomes: let totalDiscount = discount ? calcDiscount |? subtotal The benefit of DU's over inheritance for me is that you can't tie yourself to any additional details that get stuck onto Discount (since you can't extend DU's). If you have loosely coupled data you won't have sort-of-relevant methods possibly mutating shared state. The discount calc only takes a simple thing hidden away by the Discount DU, and so there are fewer plates you're trying to keep spinning in your head (all of "what could be").
- vips7L 3y agoInheritance isn’t necessary here. You achieve the same loose coupling with interfaces and you gain the benefit of your Order type not having to know how to calculate every single discount.