A common footgun I run into in Haskell codebases is the use of wildcards in pattern matching. Some enterprising developer will write some code like this:
data OnboardingStage = Welcome | PersonalInfo | PaymentInfo | Complete isGatheringInfo :: OnboardingStage -> Bool isGatheringInfo PersonalInfo = True isGatheringInfo PaymentInfo = True isGatheringInfo _ = False
isGatheringInfo might be meant for use as part of an authorization check, or to determine whether to show a progress bar,
or something else– but it's not really important. The function is correct... for now.
The problem is that this function isn't robust to changes in the
OnboardingStage type. If we add a new constructor to the type,
the compiler won't warn us that we need to update
isGatheringInfo to handle the new case. This is a problem, especially in a large codebase,
because it makes it extremely easy to miss a function that needs to be updated when a type changes.
The solution is to use exhaustive pattern matching without wildcards:
isGatheringInfo :: OnboardingStage -> Bool isGatheringInfo PersonalInfo = True isGatheringInfo PaymentInfo = True isGatheringInfo Welcome = False isGatheringInfo Complete = False
This is marginally more verbose, but it's much more robust. If we add a new constructor to
OnboardingStage, the compiler will warn us (assuming we use
data OnboardingStage = Welcome | PersonalInfo | OrganizationInfo | PaymentInfo | Complete
Will give us the following warning:
– Pattern match(es) are non-exhaustive – In an equation for ‘isGatheringInfo’: Patterns not matched: OrganizationInfo(-Wincomplete-patterns)
Coding with an eye towards evolving requirements pays dividends in the long run. It's worth taking the time to write code that's robust to change.
As with any guideline, there are exceptions. If you are pattern matching against types that are extraordinarily unlikely to change, it's probably fine to use wildcards.
Bool are unlikely to change, so it's probably fine to use wildcards when pattern matching against them. For example, the following function is
incredibly unlikely to ever have its underlying constructors change:
maybeIsTrue :: Maybe Bool -> Bool maybeIsTrue (Just True) = True maybeIsTrue _ = False
Note that in this case, the guidance still applies to the values inside the
Maybe– if were to add a new constructor to
Bool, we wouldn't get a warning that we need to update
maybeIsTrue to handle it.