If you're willing to provide a (simplified) example I would be very interested.
If you're willing to provide a (simplified) example I would be very interested.
Yes! That is exactly right, F# give you the tools to express your actual model with little/no ambiguity.
Although personally I'd rather have the different states encapsulated in separate classes than a single type that encapsulates all possible states and enforces them through the constructor.
C# can't do this because the compiler rejects putting `abstract` and `sealed` on the same type.
It's not ideal, but it works.
public class Order {
final Optional<DeliveryDetails> delivery;
public Order(Optional<DeliveryDetails> delivery) {
this.delivery = delivery;
}
}
public class DeliveryDetails {
final long deliveryTimeMs;
...
public DeliveryDetails (long deliveryTimeMs,...) {
this.deliveryTimeMs = deliveryTimeMs;
...
}
}
My IDE writes most of these lines for me. I believe C# will have similar or more succinct constrcuts.Terminology nitpick: Pattern matching is done on value constructors (or just "constructors", but at the intersection of FP and OOP that could be confusing).
"type constructor" means something like `List` (as opposed to `List[Int]`) – a generic type that hasn't been applied to any type argument(s) yet, and will "construct" a type (like `List[int]`) when you apply it.
type OrderId = OrderId of string
type DeliveryTime = DeliveryTime of long
type DeliveryDetails = { deliveryTime: DeliveryTime...}
type Order = { id: OrderId ...}
type DeliveredOrder = { id: OrderId, deliveryDetails: DeliveryDetails...}
Probably it is even better to define delivery time using the unit of measures and specifying it as ms.
What is the difference in this way?
That an order cannot ever have DeliveryDetails, while a DeliveredOrder must have DeliveryDetails.
As a bonus you can’t just pass any string as an order id (for example a description) but you need to pass an actual order id. the same is true for the deliveryTime with the adddd advantage that you won’t be able to perform operations on it with a different unit of measure.
In java or c# you would kill yourself if you try to do something similar and moreover you won’t have all the constraints specified here and the immutability automatically enforced.If I am reading the code correctly, here is the Java version (with Lombok [1]):
@Data class OrderId { final String id; }
@Data class DeliveryTime { final long time; }
@Data class DeliveryDetails { final DeliveryTime deliveryTime; }
@Data class Order { final OrderId id; }
@Data class DeliveredOrder {
final OrderId id;
final DeliveryDetails deliveryDetails;
}
I can imagine C# also having similar expressive powers.[1] http://jnb.ociweb.com/jnb/jnbJan2010.html. Lombok reduces the drudgery of writing some of the code in Java. Modern JVM languages like Scala and Kotlin have native constructs to express this.
Persistent data structures have been implemented in Java if that's what you mean by efficient mutations to immutable structures. I can't imagine such structures being hard in any language hosted on the JVM or CLR.
F#, and ML-family languages, surely have their killer features. I am only contesting the claim that the GP made that modeling a domain is a struggle in C# when compared to F#.
Imagine if someone came on this thread and claimed that they struggle to write effectful code in F# which they have been writing in Haskell. Of course, you have counter-evidence of that in all the F# programs you have written so far! I feel the same about the inability-to-domain-modeling claim.
so, if in a nominative subtyping situation like you suggest, a DeliveredOrder is-a Order, and so we can see that some subset of Orders CAN have DeliveryDetails. any method receiving an Order could receive a DeliveredOrder.
This seems reasonable, if not outright desirable.
type Order = UndeliveredOrder of UndeliveredOrder | DeliveredOrder of DeliveredOrder
And in this way you can write a function that accepts both an UndeliveredOrder and a DeliveredOrder.I was writing a little program to help glue some things together in our build/release pipeline. This tool would be deployed to the build server and get invoked by the build agent. (This could have been a script, but the complexity got to be too much to keep organized)
The tool had two halfs:
- The frontend whose job was to gather up all the 'input' from CommandLine and Env vars, do some parsing, then spit out proper types/objects.
- The backend that would interpret this data and make decisions, make some API calls and maybe copy some files.
Because of the way our our software is built, we have 5 or 6 different 'flavors' of our app that needed special treatment during build. The complexity of branching on if it was a build step or a release step, the different flavors of our app and the need to deal with input data that may or may not be there got the best of me and I spent weeks making tweaks to deal with NREs at runtime because i hadn't handled some weird case.
So i trashed the tool and rebuild the front end in F#. I spent a little time making a very accurate type representation of the data model. Including defining a lot of stuff as optional and introducing a lot of discriminated unions to represent possible branches. Then i essentially just filled in the blanks (match cases) and fixed the compile errors until every case was covered and I was done. No bugs.
You see the big, big win of F# is the default path doesn't let your cheat yourself.
You MUST handle every switch/match case.
You MUST fully construct your records.
If your function may fail, you MUST use Option to express None/Null
And you MUST handle every option type as potentially None and write handling logic
When you define your data model, just be on honest about what data needs to be where and the compiler will keep you on the straight and narrow.
There's no setup required, just start typing some F# code into the online playground and see it run immediately.
I like this one: https://fsharpforfunandprofit.com/posts/designing-for-correc...
https://fsharpforfunandprofit.com/posts/designing-for-correc...