Make illegal states unrepresentable. Use the type system as a tool to enforce invariants on the code you write. Choose your data types such that states that are illegal don’t show up as legal states in the program. Take this code representing various connection information as an example. It keeps track of relevant information in a fairly readable manner:
type connection_state =
| Connecting
| Connected
| Disconnected
type connection_info = {
state: connection_state
server: IPAddress
last_ping_time: DateTime option
last_ping_id: int option
session_id: string option
when_initiated: DateTime option
when_disconnected: DateTime option
}
On the surface these types look reasonable, but there’re some tricky invariants that need to hold about the data. For instance, if you have a last_ping_time, you should probably also have a last_ping_id and vice versa. And the session_id and when_initiated probably only makes sense when you’re connected. Similarly, when_disconnected only makes sense if and when you’ve been disconnected.
The key is that there’s nothing about the types that help you enforce all these invariants. A better approach would be to refactor the connection_info into a series of types where the invariants would be inherent in the types themselves rather than being implicit in the logic surrounding the types:
type connecting = { when_initiated: DateTime }
type connected = { last_ping: (DateTime * int) option
session_id: string }
type disconnected = { when_connected: DateTime }
type connection_state =
| Connecting of connecting
| Connected of connected
| Disconnected of disconnected
type connection_info = {
state: connection_state
server: IPAddress
}
server remains in connection_info because it applies to any of the states. The other information have been grouped together with the state it related to. The different connection_states are no longer merely a simple enumerated type but each of the different tags have content. Note also how the last_ping is now both the last_ping_time and last_ping_id. Either both are present, and grouped together, or not.
Code for exhaustiveness. This one is closely related to making illegal states unrepresentable in that you should write your code aiming at exhaustiveness guarantees. For instance, when you have a match statement, the compiler will warn you if the match is not exhaustive. The key benefit is as a refactoring tool because it guides changes in the code base. Don’t use the match all (_ –> …) because it means that if you expand on the discriminated union the compiler will not warn you.