Josh Beckmanthe 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, butparseNonEmpty
returnsNonEmpty a
, a refinement of the input type that preserves the knowledge gained in the type system. Both of these functions check the same thing, butparseNonEmpty
gives the caller access to the information it learned, whilevalidateNonEmpty
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 onlyparseNonEmpty
takes full advantage of it. If you see whyparseNonEmpty
is preferable, you understand what I mean by the mantra “parse, don’t validate.”lexi-lambda.github.ioParse, Don’t Validate