Haskell Best Practices– Wildcards aren't Your Friend.

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

The function 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 -Wincomplete-patterns). Updating OnboardingStage to:

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. Types like Either, Maybe, and 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.