Eliminating Invalid States with Haskell Algebraic Data Types
Learn how to use Haskell's Algebraic Data Types (ADTs) and pattern matching to eliminate illegal states in your domain models and leverage compiler-driven exhaustiveness checks.
30 Apr 2026, 20:33 UTC

The Cost of 'Impossible' States
In many imperative languages, domain models rely on a combination of nullable fields and boolean flags to represent different states. For example, a payment record might have an optional creditCardNumber, an optional paypalEmail, and a status string. This creates a state space where it is technically possible to have a record with both a credit card and a PayPal email, or neither, even though the business logic dictates exactly one must exist.
This “illegal state” problem leads to defensive programming: every function must start with checks to ensure the data is valid before processing it. Haskell solves this using Algebraic Data Types (ADTs), specifically Sum Types, which allow you to define a type that is exactly one of several distinct possibilities.
Modeling with Sum and Product Types
An ADT is built from two primary concepts: Product types and Sum types.
- Product Types: These are standard records or tuples. They represent “A AND B.” (e.g., a User has a Name AND an Email).
- Sum Types: These represent “A OR B.” A value of a sum type is exactly one of the defined constructors.
By nesting these, you can create a domain model that mirrors business requirements exactly. If a payment can be made via Credit Card, PayPal, or Bank Transfer, you define a single type with three constructors. The compiler then guarantees that a PaymentMethod value cannot accidentally be two things at once.
Practical Implementation: Payment Processing
Consider a system that needs to process different payment methods. Instead of using a generic object with optional fields, we define a sum type where each constructor carries only the data relevant to that specific method.
-- Define the ADT
data PaymentMethod
= CreditCard { cardNumber :: String, expiry :: String }
| PayPal { email :: String }
| BankTransfer { accountNo :: String, routingNo :: String }
deriving (Show, Eq)
-- Process the payment using pattern matching
processPayment :: PaymentMethod -> String
processPayment method = case method of
CreditCard num _ -> "Processing card " ++ num
PayPal mail -> "Redirecting to PayPal for " ++ mail
BankTransfer acc _ -> "Initiating transfer to account " ++ acc
To run this, load the code into GHCi (the Glasgow Haskell Compiler interactive environment). You can test it by calling processPayment (PayPal "user@example.com").
The Safety Net: Exhaustiveness Checking
The real power of this approach is not the definition, but the consumption. If you add a new payment method—such as CryptoWallet—to the PaymentMethod definition, the GHC compiler will issue a warning (or error, depending on flags) for every case expression that doesn't handle the new constructor.
Verification Step: To see this in action, remove the BankTransfer line from the processPayment function. When compiling with -Wall, GHC will report: Pattern match(es) are non‑exhaustive. This transforms a potential runtime crash into a compile‑time checklist.
Trade‑offs and Limitations
While ADTs provide immense safety, they introduce specific engineering challenges:
- The Pyramid of Doom: Deeply nested ADTs can lead to deeply indented pattern matching. When matching on multiple levels of nested types, the code becomes difficult to read. This is best solved by breaking matches into smaller helper functions or using guards (Boolean predicates attached to patterns).
- Cognitive Load: For developers coming from OOP, the shift from inheritance hierarchies to sum types can be jarring. Sum types are “closed”—adding a new variant requires changing the type definition and all functions that use it, whereas adding a subclass in OOP is “open.”
- Memory Considerations: Large, lazy ADT structures can lead to space leaks if the program builds up long chains of unevaluated expressions (thunks). For high‑performance data processing, using strictness annotations (
!) on fields is often necessary.
Actionable Takeaway
To improve the reliability of your Haskell domain models, audit your records for “optional” fields that are logically dependent on another field. Replace those combinations with a Sum Type. This moves the validation logic from the runtime (where it can fail) to the type system (where it is guaranteed).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.