Now, the reason why doing this recursively around :*:
might not work is, that one needs to look at the result of every validation function and then decide if the return value should be a Left ([GeneralError], UserError)
or a Right UserInput
. We cannot evaluate to a Left
value on the first validation function that fails.
The standard Applicative
behaviour for Either
is not the only reasonable behaviour for that type! As you said, when you're, eg, validating a form, you want to return a collection of all the errors that occurred, not just the first one. So here's a type that's structurally the same as Either
but has a different Applicative
instance.
newtype Validation e a = Validation (Either e a) deriving Functor
instance Semigroup e => Applicative (Validation e) where
pure = Validation . pure
Validation (Right f) <*> Validation (Right x) = Validation (Right $ f x)
Validation (Left e1) <*> Validation (Left e2) = Validation (Left $ e1 <> e2)
Validation (Left e) <*> _ = Validation (Left e)
_ <*> Validation (Left e) = Validation (Left e)
When both computations failed, the composed computation also fails, returning the two errors composed using their Semigroup
instance - both the errors, for some suitable notion of both. If both computations succeed, or only one of them fails, then Validation
behaves like Either
. So it's kind of like a Frankensteinian mishmash of the Either
and Writer
applicatives.
This instance does satisfy the Applicative
laws, but I'll leave the proof to you. Oh, and Validation
can't be made into a lawful Monad
.
Forgive me for taking the liberty of rearranging your types a bit. I'm using a common trick for reusing the structure of a record at a variety of different types: parameterise the record by a type constructor. You recover the original record by applying the template to the Identity
functor.
data UserTemplate f = UserTemplate {
name :: f Name,
age :: f Age,
email :: f Email
}
type User = UserTemplate Identity
A useful newtype: a Validator
is a function which takes an a
and returns either the a
or a monoidal summary of the errors.
newtype Validator e a = Validator { runValidator :: a -> Validation e a }
A useful class: HTraversable
is like Traversable
but for functors from the category of type constructors to Hask. (More on this in a previous question of mine.)
class HFunctor t where
hmap :: (forall x. f x -> g x) -> t f -> t g
class HFunctor t => HTraversable t where
htraverse :: Applicative a => (forall x. f x -> Compose a g x) -> t f -> a (t g)
htraverse f = hsequence . hmap f
hsequence :: Applicative a => t (Compose a g) -> a (t g)
hsequence = htraverse id
Why's HTraversable
relevant? Traversable
Classic™ allows you to sequence Applicative
effects like Validation
over homogeneous containers like lists. But a record is rather more like a heterogeneous container: a record "contains" a bunch of fields, but each field has its own type. HTraversable
is precisely the class for when you need to sequence Applicative
actions over polymorphic containers.
Another useful class generalises zipWith
to these heterogeneous containers.
class HZip t where
hzip :: (forall x. f x -> g x -> h x) -> t f -> t g -> t h
Records constructed in the fashion of UserTemplate
are traversable and zippable. (In fact they're typically HRepresentable
- an analogous higher-order notion of Representable
- which is a very useful property, though I won't dwell on it here.)
instance HFunctor UserTemplate where
hmap f (UserTemplate n a e) = UserTemplate (f n) (f a) (f e)
instance HTraversable UserTemplate where
htraverse f (UserTemplate n a e) = UserTemplate <$>
getCompose (f n) <*>
getCompose (f a) <*>
getCompose (f e)
instance HZip UserTemplate where
hzip f (UserTemplate n1 a1 e1) (UserTemplate n2 a2 e2) = UserTemplate (f n1 n2) (f a1 a2) (f e1 e2)
Hopefully it should be quite easy to see what a Generic
or Template Haskell implementation of HTraversable
and HZip
for an arbitrary record fitting this pattern would do.
So, the plan is: write Validator
s for each field and then hzip
these Validator
s along the object you want to validate. Then you can htraverse
the result to get a Validation
containing the validated object. This pattern works for field-by-field validation, per your question. If you need to look at multiple fields to validate your record, you can't use hzip
(but of course you also can't use Generic
).
type Validatable t = (HZip t, HTraversable t)
validate :: (Semigroup e, Validatable t) => t (Validator e) -> Validator e (t Identity)
validate t = Validator $ htraverse (Compose . fmap Identity) . hzip val t
where val v = runValidator v . runIdentity
A particular validator for a type such as User
basically involves picking a monoidal error and returning a record of validation functions. Here I'm defining a Monoid
for UserError
which lifts a monoidal e
point-wise through each field of the record.
type UserError e = UserTemplate (Const e)
instance Semigroup e => Semigroup (UserError e) where
x <> y = hzip (<>) x y
Now you can just define a record of validator functions.
type UserValidator = Validator ([GeneralError], UserError [FieldError])
validateEmail :: UserInput -> UserValidator Email
validateEmail i = Validator v
where v e
| '@' `elem` toString e = pure e
| otherwise = Validation $ Left ([], UserTemplate [] [] [FieldError "missing @"])
validateName :: UserInput -> UserValidator Name
validateName = ...
validateAge :: UserInput -> UserValidator Age
validateAge = ...
userValidator :: UserInput -> UserValidator User
userValidator input = validate $ UserTemplate {
name = validateName input,
age = validateAge input,
email = validateEmail input
}
You can make it easier to compose smaller validators - so that each validator doesn't need to know about the whole error structure - using lenses.