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.
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.
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.
The 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!
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").
Inheritance 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.
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.
vs 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:
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.