I am implementing a simple protocol where messages have a sequence number that must increment strictly between messages. To handle this I wrote:
newtype SequenceNo = SequenceNo Int64
deriving (Show, Eq)
validSequence :: SequenceNo -> SequenceNo -> Bool
validSequence (SequenceNo firstS) (SequenceNo secondS) = firstS + 1 == secondS
I use it something like this:
applyUpdates :: ProtocolState -> UpdateMessage -> Either String ProtocolState
applyUpdates oldState upd =
if validSequence (seqNo oldState) (updSeqNo upd)
then Right (ProtocolState {seqNo=updSeqNo upd, …})
else Left "invalid sequence"
But this has the same problem of boolean blindness that isJust :: Maybe a -> Bool
has. How do I do better?
I would just define a type isomorphic to Bool
, but with more descriptive constructor names.
date Validity = Valid | Invalid
then write a function that returns the kind of sequence represented by the arguments:
classifySequence :: SequenceNo -> SequenceNo -> Validity
classifySequence (SequenceNo x) (SequenceNo y) | x + 1 == y = Valid
| otherwise = Invalid
It's even simpler if you define an Enum
instance for your type.
newtype SequenceNo = SequenceNo Int64 deriving (Show, Read, Eq, Enum)
classifySequence :: SequenceNo -> SequenceNo -> Validity
classifySequence x y | succ x == y = Valid
| otherwise = Invalid
Either way, you can define
applyUpdates :: ProtocolState -> UpdateMessage -> Either String ProtocolState
applyUpdates oldState upd =
case validSequence (seqNo oldState) (updSeqNo upd) of
Valid -> Right (ProtocolState {seqNo=updSeqNo upd, …})
Invalid -> Left "invalid sequence"
although you might consider an explicit error type as well:
data SequenceError = InvalidSequence
applyUpdates :: ProtocolState -> UpdateMessage -> Either SequenceError ProtocolState
applyUpdates oldState upd =
case validSequence (seqNo oldState) (updSeqNo upd) of
Valid -> Right (ProtocolState {seqNo=updSeqNo upd, …})
Invalid -> Left InvalidSequence