Copyright | (c) 2014 Chris Allen Edward Kmett (c) 2018-2020 Kowainik |
---|---|
License | MPL-2.0 |
Maintainer | Kowainik <xrom.xkov@gmail.com> |
Safe Haskell | None |
Language | Haskell2010 |
Lightweight pure data validation based on Applicative
and Selective
functors.
Validation
allows to accumulate all errors instead of
short-circuting on the first error so you can display all possible
errors at once.
Common use-cases include:
- Validating each input of a form with multiple inputs.
- Performing multiple validations of a single value.
Validation
provides modular and composable interface which
means that you can implement validations for different pieces of your
data independently, and then combine smaller parts into the validation
of a bigger type. The below table illustrates main ways to combine two
Validation
s:
Typeclass | Operation ○ | Failure e ○ Failure d | Success a ○ Success b | Failure e ○ Success a | Success a ○ Failure e |
---|---|---|---|---|---|
Semigroup | <> | Failure (e <> d) | Success (a <> b) | Failure e | Failure e |
Applicative | <*> | Failure (e <> d) | Success (a b) | Failure e | Failure e |
Alternative | <|> | Failure (e <> d) | Success a | Success a | Success a |
Selective | <*? | Failure e | Selective choice | Failure e | Selective choice |
In other words, instances of different standard typeclasses provide various semantics which can be useful in different use-cases:
Semigroup
: accumulate bothFailure
andSuccess
with<>
.Monoid
:Success
that storesmempty
.Functor
: change the type insideSuccess
.Bifunctor
: change bothFailure
andSuccess
.Applicative
: apply function to values insideSuccess
and accumulate errors insideFailure
.Alternative
: return the firstSuccess
or accumulate all errors insideFailure
.Selective
: choose which validations to apply based on the value inside.
Synopsis
- data Validation e a
- isFailure :: Validation e a -> Bool
- isSuccess :: Validation e a -> Bool
- validation :: (e -> x) -> (a -> x) -> Validation e a -> x
- failures :: [Validation e a] -> [e]
- successes :: [Validation e a] -> [a]
- partitionValidations :: [Validation e a] -> ([e], [a])
- fromFailure :: e -> Validation e a -> e
- fromSuccess :: a -> Validation e a -> a
- failure :: e -> Validation (NonEmpty e) a
- failureIf :: Bool -> e -> Validation (NonEmpty e) ()
- failureUnless :: Bool -> e -> Validation (NonEmpty e) ()
- validationToEither :: Validation e a -> Either e a
- eitherToValidation :: Either e a -> Validation e a
Type
data Validation e a Source #
Validation
is a polymorphic sum type for storing either all
validation failures or validation success. Unlike Either
, which
returns only the first error, Validation
accumulates all errors
using the Semigroup
typeclass.
Usually type variables in
are used as follows:Validation
e a
e
: is a list or set of failure messages or values of some error data type.a
: is some domain type denoting successful validation result.
Some typical use-cases:
Validation
[String
] User- Either list of
String
error messages or a validated value of a customUser
type.
- Either list of
Validation
(NonEmpty
UserValidationError) User- Similar to previous example, but list of failures guaranteed to be non-empty in case of validation failure, and it stores values of some custom error type.
Failure e | Validation failure. The |
Success a | Successful validation result of type |
Instances
Bitraversable Validation Source # | Similar to Examples
|
Defined in Validation bitraverse :: Applicative f => (a -> f c) -> (b -> f d) -> Validation a b -> f (Validation c d) # | |
Bifoldable Validation Source # | Similar to Examples
|
Defined in Validation bifold :: Monoid m => Validation m m -> m # bifoldMap :: Monoid m => (a -> m) -> (b -> m) -> Validation a b -> m # bifoldr :: (a -> c -> c) -> (b -> c -> c) -> c -> Validation a b -> c # bifoldl :: (c -> a -> c) -> (c -> b -> c) -> c -> Validation a b -> c # | |
Bifunctor Validation Source # | Similar to Examples
|
Defined in Validation bimap :: (a -> b) -> (c -> d) -> Validation a c -> Validation b d # first :: (a -> b) -> Validation a c -> Validation b c # second :: (b -> c) -> Validation a b -> Validation a c # | |
NFData2 Validation Source # | |
Defined in Validation liftRnf2 :: (a -> ()) -> (b -> ()) -> Validation a b -> () # | |
(NoValidationMonadError, Semigroup e) => Monad (Validation e) Source # | ⚠️CAUTION⚠️ This instance is for custom error display only. It's not possible to implement lawful In case it is used by mistake, the user will see the following:
|
Defined in Validation (>>=) :: Validation e a -> (a -> Validation e b) -> Validation e b # (>>) :: Validation e a -> Validation e b -> Validation e b # return :: a -> Validation e a # | |
Functor (Validation e) Source # | Allows changing the value inside Examples
|
Defined in Validation fmap :: (a -> b) -> Validation e a -> Validation e b # (<$) :: a -> Validation e b -> Validation e a # | |
Semigroup e => Applicative (Validation e) Source # | This instance if the most important instance for the Examples
Implementations of all functions are lazy and they correctly work if some arguments are not fully evaluated.
|
Defined in Validation pure :: a -> Validation e a # (<*>) :: Validation e (a -> b) -> Validation e a -> Validation e b # liftA2 :: (a -> b -> c) -> Validation e a -> Validation e b -> Validation e c # (*>) :: Validation e a -> Validation e b -> Validation e b # (<*) :: Validation e a -> Validation e b -> Validation e a # | |
Foldable (Validation e) Source # |
Examples
|
Defined in Validation fold :: Monoid m => Validation e m -> m # foldMap :: Monoid m => (a -> m) -> Validation e a -> m # foldMap' :: Monoid m => (a -> m) -> Validation e a -> m # foldr :: (a -> b -> b) -> b -> Validation e a -> b # foldr' :: (a -> b -> b) -> b -> Validation e a -> b # foldl :: (b -> a -> b) -> b -> Validation e a -> b # foldl' :: (b -> a -> b) -> b -> Validation e a -> b # foldr1 :: (a -> a -> a) -> Validation e a -> a # foldl1 :: (a -> a -> a) -> Validation e a -> a # toList :: Validation e a -> [a] # null :: Validation e a -> Bool # length :: Validation e a -> Int # elem :: Eq a => a -> Validation e a -> Bool # maximum :: Ord a => Validation e a -> a # minimum :: Ord a => Validation e a -> a # sum :: Num a => Validation e a -> a # product :: Num a => Validation e a -> a # | |
Traversable (Validation e) Source # | Traverse values inside Examples
|
Defined in Validation traverse :: Applicative f => (a -> f b) -> Validation e a -> f (Validation e b) # sequenceA :: Applicative f => Validation e (f a) -> f (Validation e a) # mapM :: Monad m => (a -> m b) -> Validation e a -> m (Validation e b) # sequence :: Monad m => Validation e (m a) -> m (Validation e a) # | |
(Semigroup e, Monoid e) => Alternative (Validation e) Source # | This instance implements the behaviour when the first Examples
|
Defined in Validation empty :: Validation e a # (<|>) :: Validation e a -> Validation e a -> Validation e a # some :: Validation e a -> Validation e [a] # many :: Validation e a -> Validation e [a] # | |
NFData e => NFData1 (Validation e) Source # | |
Defined in Validation liftRnf :: (a -> ()) -> Validation e a -> () # | |
Semigroup e => Selective (Validation e) Source # |
ExamplesTo understand better, how
When user enters a password in some form, we want to check the following conditions:
As in the previous usage example with form validation, let's introduce a custom data type to represent all possible errors.
And, again, we can implement independent functions to validate all these cases:
And we can easily compose all these checks into single validation for
However, if we try using this function, we can notice a problem immediately:
Due to the nature of the You may say that check for empty password is redundant because empty
password is a special case of a short password. However, when using
This behaviour could be achieved easily if First, we need to write a function that checks whether the password is empty:
Now we can use the
With this implementation we achieved our desired behavior:
|
Defined in Validation select :: Validation e (Either a b) -> Validation e (a -> b) -> Validation e b # | |
Generic1 (Validation e :: Type -> Type) Source # | |
Defined in Validation type Rep1 (Validation e) :: k -> Type # from1 :: forall (a :: k). Validation e a -> Rep1 (Validation e) a # to1 :: forall (a :: k). Rep1 (Validation e) a -> Validation e a # | |
(Eq e, Eq a) => Eq (Validation e a) Source # | |
Defined in Validation (==) :: Validation e a -> Validation e a -> Bool # (/=) :: Validation e a -> Validation e a -> Bool # | |
(Data e, Data a) => Data (Validation e a) Source # | |
Defined in Validation gfoldl :: (forall d b. Data d => c (d -> b) -> d -> c b) -> (forall g. g -> c g) -> Validation e a -> c (Validation e a) # gunfold :: (forall b r. Data b => c (b -> r) -> c r) -> (forall r. r -> c r) -> Constr -> c (Validation e a) # toConstr :: Validation e a -> Constr # dataTypeOf :: Validation e a -> DataType # dataCast1 :: Typeable t => (forall d. Data d => c (t d)) -> Maybe (c (Validation e a)) # dataCast2 :: Typeable t => (forall d e0. (Data d, Data e0) => c (t d e0)) -> Maybe (c (Validation e a)) # gmapT :: (forall b. Data b => b -> b) -> Validation e a -> Validation e a # gmapQl :: (r -> r' -> r) -> r -> (forall d. Data d => d -> r') -> Validation e a -> r # gmapQr :: forall r r'. (r' -> r -> r) -> r -> (forall d. Data d => d -> r') -> Validation e a -> r # gmapQ :: (forall d. Data d => d -> u) -> Validation e a -> [u] # gmapQi :: Int -> (forall d. Data d => d -> u) -> Validation e a -> u # gmapM :: Monad m => (forall d. Data d => d -> m d) -> Validation e a -> m (Validation e a) # gmapMp :: MonadPlus m => (forall d. Data d => d -> m d) -> Validation e a -> m (Validation e a) # gmapMo :: MonadPlus m => (forall d. Data d => d -> m d) -> Validation e a -> m (Validation e a) # | |
(Ord e, Ord a) => Ord (Validation e a) Source # | |
Defined in Validation compare :: Validation e a -> Validation e a -> Ordering # (<) :: Validation e a -> Validation e a -> Bool # (<=) :: Validation e a -> Validation e a -> Bool # (>) :: Validation e a -> Validation e a -> Bool # (>=) :: Validation e a -> Validation e a -> Bool # max :: Validation e a -> Validation e a -> Validation e a # min :: Validation e a -> Validation e a -> Validation e a # | |
(Show e, Show a) => Show (Validation e a) Source # | |
Defined in Validation showsPrec :: Int -> Validation e a -> ShowS # show :: Validation e a -> String # showList :: [Validation e a] -> ShowS # | |
Generic (Validation e a) Source # | |
Defined in Validation type Rep (Validation e a) :: Type -> Type # from :: Validation e a -> Rep (Validation e a) x # to :: Rep (Validation e a) x -> Validation e a # | |
(Semigroup e, Semigroup a) => Semigroup (Validation e a) Source # |
Examples
|
Defined in Validation (<>) :: Validation e a -> Validation e a -> Validation e a # sconcat :: NonEmpty (Validation e a) -> Validation e a # stimes :: Integral b => b -> Validation e a -> Validation e a # | |
(Semigroup e, Semigroup a, Monoid a) => Monoid (Validation e a) Source # |
Examples
|
Defined in Validation mempty :: Validation e a # mappend :: Validation e a -> Validation e a -> Validation e a # mconcat :: [Validation e a] -> Validation e a # | |
(NFData e, NFData a) => NFData (Validation e a) Source # | |
Defined in Validation rnf :: Validation e a -> () # | |
type Rep1 (Validation e :: Type -> Type) Source # | |
Defined in Validation type Rep1 (Validation e :: Type -> Type) = D1 ('MetaData "Validation" "Validation" "validation-selective-0.0.0.0-inplace" 'False) (C1 ('MetaCons "Failure" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 e)) :+: C1 ('MetaCons "Success" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) Par1)) | |
type Rep (Validation e a) Source # | |
Defined in Validation type Rep (Validation e a) = D1 ('MetaData "Validation" "Validation" "validation-selective-0.0.0.0-inplace" 'False) (C1 ('MetaCons "Failure" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 e)) :+: C1 ('MetaCons "Success" 'PrefixI 'False) (S1 ('MetaSel ('Nothing :: Maybe Symbol) 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 a))) |
How to use
This section contains the typical Validation
usage example. Let's say we
have a form with fields where you can input your login information.
>>>
:{
data Form = Form { formUserName :: !String , formPassword :: !String } :}
This Form
data type can represent values of some text fields on the
web page or inside the GUI application. Our goal is to create a value of
the custom User
data type from the Form
fields.
First, let's define our User
type and additional newtype
s for more
type safety.
>>>
:{
newtype UserName = UserName { unUserName :: String } deriving newtype (Show) :}
>>>
:{
newtype Password = Password { unPassword :: String } deriving newtype (Show) :}
>>>
:{
data User = User { userName :: !UserName , userPassword :: !Password } deriving stock (Show) :}
We can easily create a User
from the Form
in the unsafe way by wrapping
each form field into the corresponding newtype
:
>>>
:{
unsafeUserFromForm :: Form -> User unsafeUserFromForm Form{..} = User { userName = UserName formUserName , userPassword = Password formPassword } :}
However, this conversion is unsafe (as name suggests) since Form
can
contain invalid data. So, before creating a User
we want to check
whether all Form
fields satisfy our preconditions. Specifically:
- User name must not be empty.
- Password should be at least 8 characters long.
- Password should contain at least 1 digit.
Validation
offers modular and composable way of defining and
outputting all validation failures which means:
- Modular: define validation checks for different fields independently.
- Composable: combine smaller validations easily into a validation of a bigger type.
Before implementing Form
validation, we need to introduce a type for
representing our validation errors. It is a good practice to define
all possible errors as a single sum type, so let's go ahead:
>>>
:{
data FormValidationError = EmptyName | ShortPassword | NoDigitPassword deriving stock (Show) :}
With Validation
we can define checks for individual fields
independently and compose them later. First, let's start with defining
validation for the name:
>>>
:{
validateName :: String -> Validation (NonEmpty FormValidationError) UserName validateName name = UserName name <$ failureIf (null name) EmptyName :}
You can notice a few things about this function:
- All errors are collected in
NonEmpty
, since we want to have guarantees that in case of errors we have at least one failure. - It wraps the result into
UserName
to tell that validation is passed.
Let's see how this function works:
>>>
validateName "John"
Success "John">>>
validateName ""
Failure (EmptyName :| [])
Since Validation
provides modular interface for defining checks,
we now can define all validation functions for the password
separately:
>>>
:{
validateShortPassword :: String -> Validation (NonEmpty FormValidationError) Password validateShortPassword password = Password password <$ failureIf (length password < 8) ShortPassword :}
>>>
:{
validatePasswordDigit :: String -> Validation (NonEmpty FormValidationError) Password validatePasswordDigit password = Password password <$ failureUnless (any isDigit password) NoDigitPassword :}
After we've implemented validations for different Form
fields, it's
time to combine them together! Validation
offers several ways to
compose different validations. These ways are provided via different
instances of common Haskell typeclasses, specifically:
Semigroup
allows combining values inside both Failure
and
Success
but this requires both values to implement the Semigroup
instance. This doesn't fit our goal, since Password
can't have a
reasonble Semigroup
instance.
Alternative
returns first Success
or combines all Failure
s. We
can notice that Alternative
also doesn't work for us here.
In our case we are interested in collecting all possible errors and
returning Success
only when all checks are passed. Fortunately,
Applicative
is exactly what we need here. So we can use the *>
operator to compose all checks for password:
>>>
:{
validatePassword :: String -> Validation (NonEmpty FormValidationError) Password validatePassword password = validateShortPassword password *> validatePasswordDigit password :}
Let's see how it works:
>>>
validatePassword "abcd"
Failure (ShortPassword :| [NoDigitPassword])>>>
validatePassword "abcd1"
Failure (ShortPassword :| [])>>>
validatePassword "abcd12345"
Success "abcd12345"
After we've implemented validations for all fields, we can compose
them together to produce validation for the whole User
. As before,
we are going to use the Applicative
instance:
>>>
:{
validateForm :: Form -> Validation (NonEmpty FormValidationError) User validateForm Form{..} = User <$> validateName formUserName <*> validatePassword formPassword :}
And it works like a charm:
>>>
validateForm (Form "" "")
Failure (EmptyName :| [ShortPassword,NoDigitPassword])>>>
validateForm (Form "John" "abc")
Failure (ShortPassword :| [NoDigitPassword])>>>
validateForm (Form "Jonh" "qwertypassword")
Failure (NoDigitPassword :| [])>>>
validateForm (Form "Jonh" "qwertypassword123")
Success (User {userName = "Jonh", userPassword = "qwertypassword123"})
Interface functions
isFailure :: Validation e a -> Bool Source #
Predicate on if the given Validation
is Failure
.
>>>
isFailure (Failure 'e')
True>>>
isFailure (Success 'a')
False
isSuccess :: Validation e a -> Bool Source #
Predicate on if the given Validation
is Success
.
>>>
isSuccess (Success 'a')
True>>>
isSuccess (Failure 'e')
False
validation :: (e -> x) -> (a -> x) -> Validation e a -> x Source #
Transforms the value of the given Validation
into x
using provided
functions that can transform Failure
and Success
value into the resulting
type respectively.
>>>
let myValidation = validation (<> " world!") (show . (* 10))
>>>
myValidation (Success 100)
"1000">>>
myValidation (Failure "Hello")
"Hello world!"
failures :: [Validation e a] -> [e] Source #
Filters out all Failure
values into the new list of e
s from the given
list of Validation
s.
Note that the order is preserved.
>>>
failures [Failure "Hello", Success 1, Failure "world", Success 2, Failure "!" ]
["Hello","world","!"]
successes :: [Validation e a] -> [a] Source #
Filters out all Success
values into the new list of a
s from the given
list of Validation
s.
Note that the order is preserved.
>>>
successes [Failure "Hello", Success 1, Failure "world", Success 2, Failure "!" ]
[1,2]
partitionValidations :: [Validation e a] -> ([e], [a]) Source #
Redistributes the given list of Validation
s into two lists of e
s and
e
s, where the first list contains all values of Failure
s and the second
one — Success
es correspondingly.
Note that the order is preserved.
>>>
partitionValidations [Failure "Hello", Success 1, Failure "world", Success 2, Failure "!" ]
(["Hello","world","!"],[1,2])
fromFailure :: e -> Validation e a -> e Source #
Returns the contents of a Failure
-value or a default value otherwise.
>>>
fromFailure "default" (Failure "failure")
"failure">>>
fromFailure "default" (Success 1)
"default"
fromSuccess :: a -> Validation e a -> a Source #
Returns the contents of a Success
-value or a default value otherwise.
>>>
fromSuccess 42 (Success 1)
1>>>
fromSuccess 42 (Failure "failure")
42
NonEmpty
combinators
When using Validation
, we often work with the NonEmpty
list of errors, and
those lists will be concatenated later.
The following functions aim to help with writing more concise code.
For example, instead of (perfectly fine) code like:
>>>
:{
validateNameVerbose :: String -> Validation (NonEmpty String) String validateNameVerbose name | null name = Failure ("Empty Name" :| []) | otherwise = Success name :}
one can write simply:
>>>
:{
validateNameSimple :: String -> Validation (NonEmpty String) String validateNameSimple name = name <$ failureIf (null name) "Empty Name" :}
failure :: e -> Validation (NonEmpty e) a Source #
failureUnless :: Bool -> e -> Validation (NonEmpty e) () Source #
Returns a Failure
unless the given predicate is True
.
Returns
in case of the predicate is satisfied.Success
()
Similar to failureIf
with the reversed predicate.
failureUnless
p ≡failureIf
(not p)
>>>
let shouldFail = (==) "I am a failure"
>>>
failureUnless (shouldFail "I am a failure") "doesn't matter"
Success ()>>>
failureUnless (shouldFail "I am NOT a failure") "I told you so"
Failure ("I told you so" :| [])
Either
conversion
Validation
is usually compared to the Either
data type due to the similarity
in structure, nature and use case. Here is a quick table you can relate to, in
order to see the main properties and differences between these two data types:
Either | Validation | |
---|---|---|
Error result | Left | Failure |
Successful result | Right | Success |
Applicative instance | Stops on the first Left | Aggregates all Failure s |
Monad instance | Lawful instance | Cannot exist |
Comparison in example
For the sake of better illustration of the difference between Either
and
Validation
, let's go through the example of how parsing is done with the usage of
these types.
Our goal is to parse two given String
s and return their sum in case if both of
them are valid Int
s. If any of the inputs is failing to be parsed we should
return the ParseError
which we are introducing right now:
>>>
:{
newtype ParseError = ParseError { nonParsedString :: String } deriving stock (Show) :}
Let's first implement the parsing of single input in the Either
context:
>>>
:{
parseEither :: String -> Either ParseError Int parseEither input = case readMaybe @Int input of Just x -> Right x Nothing -> Left $ ParseError input :}
And the final function for Either
looks like this:
>>>
:{
parseSumEither :: String -> String -> Either ParseError Int parseSumEither str1 str2 = do let x = parseEither str1 let y = parseEither str2 liftA2 (+) x y :}
Let's now test it in action.
>>>
parseSumEither "1" "2"
Right 3>>>
parseSumEither "NaN" "42"
Left (ParseError {nonParsedString = "NaN"})>>>
parseSumEither "15" "Infinity"
Left (ParseError {nonParsedString = "Infinity"})>>>
parseSumEither "NaN" "infinity"
Left (ParseError {nonParsedString = "NaN"})
Note how in the case of both failed parsing we got only the first NaN
.
To finish our comparison, let's implement the same functionality using
Validation
properties.
>>>
:{
parseValidation :: String -> Validation (NonEmpty ParseError) Int parseValidation input = case readMaybe @Int input of Just x -> Success x Nothing -> failure $ ParseError input :}
>>>
:{
parseSumValidation :: String -> String -> Validation (NonEmpty ParseError) Int parseSumValidation str1 str2 = do let x = parseValidation str1 let y = parseValidation str2 liftA2 (+) x y :}
It looks almost completely identical except for the resulting type —
. But let's see if they behave the
same way:Validation
(NonEmpty
ParseError) Int
>>>
parseSumValidation "1" "2"
Success 3>>>
parseSumValidation "NaN" "42"
Failure (ParseError {nonParsedString = "NaN"} :| [])>>>
parseSumValidation "15" "infinity"
Failure (ParseError {nonParsedString = "infinity"} :| [])>>>
parseSumValidation "NaN" "infinity"
Failure (ParseError {nonParsedString = "NaN"} :| [ParseError {nonParsedString = "infinity"}])
As expected, with Validation
we got all parse Failure
s we received on
the way.
Combinators
We are providing several functions for better integration with the Either
related code in this section.
validationToEither :: Validation e a -> Either e a Source #
Transform a Validation
into an Either
.
>>>
validationToEither (Success "whoop")
Right "whoop"
>>>
validationToEither (Failure "nahh")
Left "nahh"
eitherToValidation :: Either e a -> Validation e a Source #
Transform an Either
into a Validation
.
>>>
eitherToValidation (Right "whoop")
Success "whoop"
>>>
eitherToValidation (Left "nahh")
Failure "nahh"