Skirmantas Kligys
Skirmantas Kligys

Reputation: 826

Validations in Haskell

I have a few nested records that I need to validate, and I wonder what is an idiomatic Haskell way to do it.

To simplify:

data Record = Record {
  recordItemsA :: [ItemA],
  recordItemB :: ItemB
} deriving (Show)

data ItemA {
  itemAItemsC :: [ItemC]
} deriving (Show)

Requirements are:

I currently have code that feels awkward:

type ErrorMsg = String

validate :: Record -> [ErrorMsg]
validate record =
  recordValidations ++ itemAValidations ++ itemBValidations
  where
    recordValidations :: [ErrorMsg]
    recordValidations = ensure (...) $
      "Invalid combination: " ++ (show $ recordItemsA record) ++ " and " ++ (show $ recordItemsB record)
    itemAValidations :: [ErrorMsg]
    itemAValidations = concat $ map validateItemA $ recordItemsA record
    validateItemA :: ItemA -> [ErrorMsg]
    validateItemA itemA = ensure (...) $
      "Invalid itemA: " ++ (show itemA)
    itemBValidations :: [ErrorMsg]
    itemBValidations = validateItemB $ recordItemB record
    validateItemB :: ItemB -> [ErroMsg]
    validateItemB itemB = ensure (...) $
      "Invalid itemB: " ++ (show itemB)

ensure :: Bool -> ErrorMsg -> [ErrorMsg]
ensure b msg = if b then [] else [msg]

Upvotes: 11

Views: 2280

Answers (4)

jberryman
jberryman

Reputation: 16645

One thing you might consider trying is, rather than validating your data afterwards, use lenses from the excellent fclabels package as your interface to your data (rather than pattern-matching/type constructors) to ensure that your data is always correct.

Check out the variant that supports failure here and build your lens by passing a setter and getter that do some validation on the datatype to the lens function.

If you need some more complicated error reporting or whatnot, take a look at the implementation of the Maybe variant of lens and define your lens in terms of the abstract interface.

Upvotes: 0

ehird
ehird

Reputation: 40807

What you have already is basically fine, it just needs some clean-up:

  • The sub-validations should be top-level definitions, as they're fairly involved. (By the way, type signatures on where clause definitions are usually omitted.)
  • Lack of consistent naming convention
  • Lots of (++)s in sequence can get ugly — use concat (or perhaps unwords) instead
  • Minor formatting quirks (there are some superfluous parentheses, concat . map f is concatMap f, etc.)

The product of all this:

validateRecord :: Record -> [ErrorMsg]
validateRecord record = concat
  [ ensure (...) . concat $
      [ "Invalid combination: ", show (recordItemsA record)
      , " and ", show (recordItemB record)
      ]
  , concatMap validateItemA $ recordItemsA record
  , validateItemB $ recordItemB record
  ]

validateItemA :: ItemA -> [ErrorMsg]
validateItemA itemA = ensure (...) $ "Invalid itemA: " ++ show itemA

validateItemB :: ItemB -> [ErrorMsg]
validateItemB itemB = ensure (...) $ "Invalid itemB: " ++ show itemB

I think that's pretty good. If you don't like the list notation, you can use the Writer [ErrorMsg] monad:

validateRecord :: Record -> Writer [ErrorMsg] ()
validateRecord record = do
  ensure (...) . concat $
    [ "Invalid combination: ", show (recordItemsA record)
    , " and ", show (recordItemB record)
    ]
  mapM_ validateItemA $ recordItemsA record
  validateItemB $ recordItemB record

validateItemA :: ItemA -> Writer [ErrorMsg] ()
validateItemA itemA = ensure (...) $ "Invalid itemA: " ++ show itemA

validateItemB :: ItemB -> Writer [ErrorMsg] ()
validateItemB itemB = ensure (...) $ "Invalid itemB: " ++ show itemB

ensure :: Bool -> ErrorMsg -> Writer [ErrorMsg] ()
ensure b msg = unless b $ tell [msg]

Upvotes: 5

nponeccop
nponeccop

Reputation: 13677

Read the 8 ways to report errors in Haskell article. For your particular case, as you need to collect all errors and not only the first one, the approach with Writer monad suggested by @ehird seems to fit best, but it's good to know other common approaches.

Upvotes: 5

pat
pat

Reputation: 12749

Building on @ehird's answer, you could introduce a Validate typeclass:

class Validate a where
  validate :: a -> [ErrorMsg]

instance Validate a => Validate [a] where
  validate = concatMap validate

instance Validate Record where
  validate record = concat
    [ ensure (...) . concat $
      [ "Invalid combination: ", show (recordItemsA record)
      , " and ", show (recordItemB record)
      ]
    , validate $ recordItemsA record
    , validate $ recordItemB record
    ]

instance Validate ItemA where
  validate itemA = ensure (...) $ "Invalid itemA: " ++ show itemA

instance Validate ItemB where
  validate itemB = ensure (...) $ "Invalid itemB: " ++ show itemB

Upvotes: 0

Related Questions