In the first part of this series, we went through a basic introduction to Functor and Applicative Functor. In this second part, we will go through an exercise to show how to use them to perform input data validation.
Create the types:
Create the functions:
makeAddressthat takes a String and validates that it contains a '@'. It returns an
Addressvalue wrapped in a
Justif it is valid or
makeBodythat takes a String and validates that it is not empty. It returns a
Bodyvalue wrapped in a
Justif it is valid or
makeEmailthat takes as arguments three strings,
body. It returns an email instance wrapped in
type FromAddress = Address type ToAddress = Address data Address = Address String data Body = Body String data Email = Email FromAddress ToAddress Body
ToAddress are type aliases to the
Address type so that it’s easier to identify which one is which in the Email data constructor.
It is worth noting that the data constructors for our types
For instance the
Email :: Address -> Address -> Body -> Email
makeAddress :: String -> Maybe Address makeAddress address = fmap Address (validateContains '@' address) makeBody :: String -> Maybe Body makeBody body = fmap Body (validateNonEmpty body) makeEmail :: String -> String -> String -> Maybe Email makeEmail from to body = case makeAddress from of Nothing -> Nothing Just fromAddress -> case makeAddress to of Nothing -> Nothing Just toAddress -> case makeBody body of Nothing -> Nothing Just body -> Just (Email fromAddress toAddress body)
Auxiliary functions to validate input.
validateNonEmpty :: String -> Maybe String validateNonEmpty  = Nothing validateNonEmpty xs = Just xs validateContains :: Char -> String -> Maybe String validateContains x xs | elem x xs = Just xs | otherwise = Nothing
makeBody successfully leveraged the functoriality of Maybe. We were able to apply the validation function to obtain a
Maybe String to then fmap it to
Maybe Address and
Maybe Body respectively.
On the other hand, the
makeEmail implementation is much more involved and cumbersome. Let's see why we had to do the break down of Maybe values manually.
If we try to fmap the
makeBody, the expression has the following type:
fmap Email (makeAddress "firstname.lastname@example.org") :: Maybe (Address -> Body -> Person)
Examining the type of the expression, we can see that we have partially applied the
Address value inside the Maybe structure. However, it is still waiting for two more arguments in order to produce the desired result type for
If we now try to apply the second argument by applying fmap again we get a type error, as
fmap does not take a function embedded in a structure, but a function on its own.
fromAddress :: Maybe Address fromAddress = makeAddress "email@example.com" toAddress :: Maybe Address toAddress = makeAddress "firstname.lastname@example.org" emailWithFromApplied :: Maybe (Address -> Body -> Email) emailWithFromApplied = fmap Email fromAddress -- does not compile emailWithFromAndToApplied :: Maybe (Body -> Email) emailWithFromAndToApplied = fmap emailWithFromApplied toAddress
The problem here is that the function that we are passing to
fmap in the case of
emailWithFromAndToApplied is wrapped in a Maybe structure and
fmap does not accept a function wrapped in a structure.
Applicatives to the rescue! They let us do exactly what we need, apply a function inside a structure to a value inside a structure of the same type.
emailWithFromAndToApplied :: Maybe (Body -> Email) emailWithFromAndToApplied = emailWithFromApplied <*> toAddress
Now we have successfully applied the second argument to the
Body argument, so let's apply it using
<*> once again:
body :: Maybe Body body = makeBody "Haskell rocks." emailFullyApplied :: Maybe Email emailFullyApplied = emailWithFromAndToApplied <*> body
We just implemented the function
emailFullyApplied by leveraging the power of Functor and Applicative. Let's break down what we just did:
makeAddressprovides. This resulted in the
fromAddress. The rest of the function,
Address -> Body -> Email, is now embedded or wrapped inside a Maybe structure.
toAddress, obtaining the
body, obtaining the desired result.
The implementation did not look as bad as the first attempt, but just because we broke it down into several intermediate results. If we were to inline the implementation of the function
emailFullyApplied we would get:
emailFullyApplied :: Maybe Email emailFullyApplied = ((fmap Email (makeAddress "email@example.com")) <*> makeAddress "firstname.lastname@example.org") <*> makeBody "Haskell rocks"
The Applicative package in Haskell also provides an infix operator
fmap that makes the implementation a bit nicer, by getting rid of the parenthesis. It behaves as
fmap, but it is placed between its two arguments:
emailFullyApplied :: Maybe Email emailFullyApplied = Email <$> makeAddress "email@example.com" <*> makeAddress "firstname.lastname@example.org" <*> makeBody "Haskell rocks"
We can go back to the original
makeEmail function by extracting the hardcoded values as parameters:
makeEmail :: String -> String -> String -> Maybe Email makeEmail from to body = Email <$> makeAddress from <*> makeAddress to <*> makeBody body
It is worth noting that this implementation of
makeEmail, apart from being far nicer than the original one, is much easier to extend if we were to add more arguments or fields to the
Map a function using
<$>to partially apply it and to embed it inside a structure, then apply it to the rest of it arguments using
This pattern is so common in Haskell that the Applicative package provides several utility functions,
liftA3... The liftAn functions will take a function of n arguments and will apply it to all its arguments wrapped in a structure f.
For instance, liftA3, takes a function of three arguments and three values wrapped in a structure f and it applies the function to the values as we did in the
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d`
Substituting our types:
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d liftA3 :: (Address -> Address -> Body -> Email) -> Maybe Address -> Maybe Address -> Maybe Body -> Maybe Email
makeEmail to make use of liftA3:
makeEmail :: String -> String -> String -> Maybe Email makeEmail from to body = liftA3 Email (makeAddress from) (makeAddress to) (makeBody body)
We just saw how to use the Maybe Functor and Applicative Functor to validate input data, however, Maybe cannot offer any information about the error.
We will see how we can signal errors and also accumulate all the errors occurred during the validation process using the
Validation data type. This is a more practical scenario for real-life applications, as it is often required to return some information about the errors.
The data type
Validation is very similar to
Either. You may be familiar with
Either as it is quite a common type in most functional languages.
data Either a b = Left a | Right b data Validation err a = Failure err | Success a
The definition of both types shows that
Validation are indeed identical, they just have different names for their type and data constructors.
Either is a general purpose data type. Validation is a variation of Either exclusively for validation purposes that accumulates all the errors. This difference is not visible in the definitions of the data types, but it is in their Applicative instances.
We will be using the Validation package Haskell, which defines the
AccValidation data type that accumulates errors in a given type.
Eitherjust short-circuits as soon as there is an error, as Maybe does, but it can carry with it information about the actual error.
AccValidationaccumulates all the errors in a given type, usually List.
AccValidation accumulates all the errors using the Semigroup typeclass. A semigroup is an abstraction that combines two arguments of the same type into a single one, as Monoid does, but it does not have an identity element. We are going to show how to combine all the errors using
AccValidation and List. List has both a Monoid instance and a Semigroup instance.
AccValidation data type and its Applicative instance are defined as follows:
data AccValidation err a = AccFailure err | AccSuccess a Semigroup err => Applicative (AccValidation err)
Let's see how we can use it in code.
First, we need to define an
Error data type for our implementation:
data Error = EmptyBody | AddressMustContain String deriving (Eq, Show)
And three new functions
validateEmail that return information about the error in case there is one:
validateAddress :: String -> AccValidation [Error] Address validateAddress address = maybeToValidation error (makeAddress address) where error = AddressMustContain "@" validateBody :: String -> AccValidation [Error] Body validateBody body = maybeToValidation EmptyBody (makeBody body) validateEmail :: String -> String -> String -> AccValidation [Error] Email validateEmail from to body = Email <$> validateAddress from <*> validateAddress to <*> validateBody body
Auxiliary function to convert Maybe to AccValidation:
maybeToValidation :: Error -> Maybe a -> AccValidation [Error] a maybeToValidation error Nothing = AccFailure [error] maybeToValidation _ (Just x) = AccSuccess x
validateEmail :: String -> String -> String -> AccValidation [Error] Email validateEmail from to body = liftA3 Email (validateAddress from) (validateAddress to) (validateBody body)
Note how the implementation of
validateEmail is identical to the one we defined earlier for
makeEmail. The type signature has changed, as it now returns an
AccValidation instead of a
Maybe, but given that both types are Applicatives Functors we don’t need to change the implementation to change the behaviour.
To see how errors accumulate, let's write the following two functions that feed sample data to
allWrong :: AccValidation [Error] Email allWrong = validateEmail "wrong" "alsoWrong" "" allGood :: AccValidation [Error] Email allGood = validateEmail "email@example.com" "firstname.lastname@example.org" "Haskell rocks"
Evaluating both expresions in the REPL:
Prelude> print allWrong AccFailure [AddressMustContain "@",AddressMustContain "@",EmptyBody] Prelude> print allGood AccSuccess (Email (Address "email@example.com") (Address "firstname.lastname@example.org") (Body "Haskell rocks"))
As a further observation, note how the type signatures for
$, the function application operator,
<$>, for fmap, and
<*>, for apply, are very similar. They all play around adding an extra layer of structure to the arguments, a function and a value, and the return type. They all are function application.
$ :: (a -> b) -> a -> b <$> :: (a -> b) -> f a -> f b <*> :: f (a -> b) -> f a -> f b
Software es nuestra pasión.
Somos Software Craftspeople. Construimos software bien elaborado para nuestros clientes, ayudamos a los/as desarrolladores/as a mejorar en su oficio a través de la formación, la orientación y la tutoría. Ayudamos a las empresas a mejorar en la distribución de software.