Good Types Reduce Bugs
It is largely accepted in the Swift community that the type system reduces bugs. There are trivial examples of the compiler catching mistakes such as passing the wrong numeric type to function. This class of bug is found with no help needed from developer. True enough. But when the developer does make the effort to carefully design types, then the more profound benefits of strong, static typing become available.
A Naive Approach
Here’s one way to represent the state of a network connection.
The ConnectionState
is a simple enum with three possible cases. The ConnectionInfo
struct provides information about different aspects of a connection. The state
and the server
fields are required, the rest are optional. The optional fields are not populated willy-nilly: they operate under strict invariants:
lastPingTime
andlastPingId
are used to keep the connection alive. They are optional because they should only be present whenstate
isconnected
, and in that case both must be present.sessionId
is a unique identifier for each new connection and it should only be present whenstate
isconnected
.initiated
is a timestamp used to determine whether the attempt to connect should continue. It should only be present whenstate
isconnecting
.- Similarly,
disconnected
is a timestamp but is used to record when the connect disconnected. It should only be present whenstate
isdisconnected
.
There are numerous ways this code can break. Preventing that requires precise documentation. Each client must be subjected to complex tests to confirm it is using this code correctly. Therefore, it requires continuous vigilance on the part of developers to maintain these invariants.
A Better Solution
What we want is to be able to write the code once and forget it, confident that it works and cannot be abused. That’s the dream anyway, and we can get close if we think carefully about our types. As it turns out, these invariants can be baked directly into the code. Then, you don’t need to document how the code works, that would be like explaining how Swift itself works. It doesn’t need complex tests, because you don’t need to test Swift.
ConnectionInfo
has simplified down to its bare requirements: it must have a state
and an server
address. The previous optional fields have been moved to associated types on their appropriate cases.
- When the
state
isconnecting
, there must be a date stored right there in the case. - When the
state
isconnected
, there must be asessionId
instance available just the toconnected
instance. There may be an instance ofPing
as well, only if any pings were sent. But you know thatlastPing
, if it is not nil, will have both atime
and anid
. - When
state
isdisconnected
, you can depend on thedisconnected
timestamp to be available.
Summary
Now that the invariants are part of the types themselves, the compiler can detect and reject code that violates these invariants. This is less work and more reliable than trying to maintain these invariants by hand. Both examples are valid Swift, but the second is superior because it shifts more overhead from the developer to the language.
Further Reading
- Enumerations See the part especially about Associated Values.
- OCaml for the Masses (…where I stole my example. There is much other insightful information here too. It is about OCaml but is nevertheless relevant to Swift.)
- Serializing Enums with Associated Values (Shameless!)
Encoding and Decoding Enums with Associated Values in Swift 4
How to update your Cocoapod