the difference between validation and parsing lies almost entirely in how information is preserved. Consider the following pair of functions:
validateNonEmpty :: [a] -> IO ()
validateNonEmpty (:) = pure ()
validateNonEmpty [] = throwIO $ userError “list cannot be empty”
parseNonEmpty :: [a] -> IO (NonEmpty a)
parseNonEmpty (x:xs) = pure (x:|xs)
parseNonEmpty [] = throwIO $ userError “list cannot be empty”
These two functions are nearly identical: they check if the provided list is empty, and if it is, they abort the program with an error message. The difference lies entirely in the return type: validateNonEmpty
always returns ()
, the type that contains no information, but parseNonEmpty
returns NonEmpty a
, a refinement of the input type that preserves the knowledge gained in the type system. Both of these functions check the same thing, but parseNonEmpty
gives the caller access to the information it learned, while validateNonEmpty
just throws it away.
These two functions elegantly illustrate two different perspectives on the role of a static type system: validateNonEmpty
obeys the typechecker well enough, but only parseNonEmpty
takes full advantage of it. If you see why parseNonEmpty
is preferable, you understand what I mean by the mantra “parse, don’t validate.”
Parsing returns a new piece of data. Parsing progresses the information processing, not just the program.