Reputation: 826
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:
ItemsA
against ItemB
String
s are sufficient to represent errorsI 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
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
Reputation: 40807
What you have already is basically fine, it just needs some clean-up:
where
clause definitions are usually omitted.)(++)
s in sequence can get ugly — use concat
(or perhaps unwords
) insteadconcat . 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
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
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