I am currently reading Haskell Programming from first principles, also known as the Haskell book, as part of my journey to learn functional programming. The book has helped me to acquire many new intuitions and I will share two of them, Functors and Applicative Functors.
To begin with, we will start with a basic introduction to Functors and Applicative Functors, exploring the interface they provide. This will set the ground for the second part of this series, where we will show how to leverage Applicative Functors to validate input data.
We will be following some code samples written in Haskell. It is not required to know Haskell to follow this series, we will be explaining the code snippets in detail.
In order to define Applicative, we first need to define another structure that is part of its core, Functor. The Applicative structure is built on top of a functor.
Functor and Applicative are mathematical abstractions, but we are going to expose a very relaxed definition, as the focus of this post is to show how to use Applicative Functors for data validation. That being said, it is very important to understand that both Functor and Applicative are data structures defined together with a series of laws. We should test and verify that the implementations we pick are fully compliant with the laws. They ensure that the properties obtained by translating these concepts from mathematics into code are preserved, such as safe composition of functions and structures.
A Functor is a higher-kinded type, that is a generic data structure which takes one type argument. Functor defines the structure together with a function
map to operate with the content of the data structure.
map is a higher-order function that applies a given function to the content of the structure, we will explore its type signature soon.
Functor provides a single way to interact with the content of the data structure, and that is by applying a function to the content.
Functor is defined in Haskell as the Functor typeclass, which provides the
fmap function. It is worth noting that
fmap is the same as the
map function defined in other languages.
The definition of the Functor typeclass is as follows:
(1) class Functor f where fmap :: (a -> b) -> f a -> f b (2)
fmapfunction. It takes two parameters, a function
a -> b, and a structure f that takes one type argument of type a,
f a, and returns a new structure f that takes one type argument of type b,
There are plenty of Functor instances defined in Haskell, but we will show the two canonical ones,
Maybe. This last one is also known as Optional in other languages.
The definition of the data type List,
, and Maybe are as follows:
data  a =  | a : [a] data Maybe a = Nothing | Just a
If we substitute the f in the Functor definition for List and Maybe respectively, we get the following:
fmap :: (a -> b) -> f a -> f b fmap :: (a -> b) -> [ a ] -> [ b ] fmap :: (a -> b) -> Maybe a -> Maybe b
As we can see, the type signature of the
fmap function shows that it converts a given structure f that takes one type argument of type a, to the same structure, but with a type argument of type b.
List, applies the function
a -> bto each of its elements, converting
Maybe, it is a bit different, given the nature of the Maybe structure. The Maybe structure is used to signal absence and its representation may contain one element of type a if the value is
Just, signalling existence, or no elements at all if the value is empty, represented as
Nothing, signalling absence. Therefore, the implementation of
Maybeapplies the function
a -> bto the element inside
Justor simply does nothing if the value is empty, returning a
Nothingvalue. If the value is
Nothingit is literally impossible to apply the function as there is no value to apply it to.
Note how applying
fmap has a different connotation depending on the structure that implements it, even when conceptually is the same operation.
For example, apply (+1) to each element of a list:
fmap (+1) [1..5] [2, 3, 4, 5, 6]
Apply (+1) to Just and Nothing
fmap (+1) (Just 1) Just 2 fmap (+1) Nothing Nothing
Applicative is defined in Haskell also as a typeclass.
(1) (2) class Functor f => Applicative f where (3) pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b (4)
puretakes an argument of type a and embeds it in a structure f.
<*>, read as
apply, is defined as an infix operator, meaning that in order to apply it we have to place it between its two arguments; we will see an example shortly.
<*>also takes two arguments as
fmapdoes, but in this case, both arguments, the function and the value, are wrapped in a structure f.
Let's put the type signatures of
<*> side by side, so that we can observe the similarities and differences between them:
fmap :: (a -> b) -> f a -> f b <*> :: f (a -> b) -> f a -> f b
The signatures for both functions are quite similar, but as we can see the difference is that in the case of
<*> not only
a is wrapped in the structure
f, but also the function
a -> b.
As a note, it is worth mentioning that Applicatives are also known as Monoidal Functors. If you are familiar with the concept of Monoid, you may see that when dealing with the
<*> function, we have two arguments (a function, and a value) wrapped in a structure of the same type, so we have to be able to somehow merge or combine both structures into a single one in order to produce a result.
Let's substitute the f for List and Maybe as we did previously.
pure :: a -> [a] <*> :: [a -> b] -> [a] -> [b]
The implementation of
pure for List takes a value of type a and wraps it in a List.
Prelude> pure 5 :: [Int] 
We are going to prepend expressions with "Prelude>" to indicate that we are evaluating them, as ghci does. ghci is the interactive environment for Haskell.
:: [Int] is needed in order to tell the compiler that we want it to use the Applicative instance for List. This is needed because we are executing this line in isolation, if we were writing this function combined with others that carry a type, the compiler would infer the type and the typeclass instance for us.
The implementation of
<*> for List takes a list of functions,
[a -> b], and a list of values of type a,
[a], and returns a list of values of type b,
[b]. It applies every function contained in the first argument list to every value in the second argument list, returning a list with the results of applying every function to every element.
As in the
fmap implementation for List,
<*> applies the given function to each element of the given List, but in this case, it takes a List of functions, so it applies them all.
Prelude> [(+1), (*2)] <*> [10, 100, 1000] = [1 + 10, 1 + 100, 1 + 1000, 2 * 10, 2 * 100, 2 * 1000] -- This intermediate result does not appear in ghci = [11 , 101 , 1001 , 20 , 200 , 2000]
pure :: a -> Maybe a <*> :: Maybe (a -> b) -> Maybe a -> Maybe b
The implementation of
pure for Maybe takes a value of type a and wraps it in a
Prelude> pure 5 :: Maybe Int Just 5
As before, the
:: Maybe Int tells the compiler to use the Applicative instance for Maybe.
The implementation of
<*> for Maybe takes a function
a -> b wrapped in a Maybe structure,
Maybe (a -> b), and a value of type Maybe a and it returns a value of type Maybe b. As in
fmap, the result will vary depending on the Maybe value, but in this case, the function is also wrapped in a Maybe structure so it may or may not be there.
There are four possible combinations, but only one of them produces a success scenario:
Prelude> Just (\x -> x + 1) <*> Just 1 Just 2 Prelude> Nothing <*> Just 1 Nothing Prelude> Just (\x -> x + 1) <*> Nothing Nothing Prelude> Nothing <*> Nothing Nothing
<*> can only apply the function in the first argument to the value in the second argument if both are
Just, otherwise it either does not have a function, a value or none of them.
As we just saw Applicatives can be used for many different purposes as the meaning of applying the function
<*> changes depending on the structure that implements it:
If the structure is List then it applies a list of functions to a list of values; If the structure is Maybe it applies the function to the value if both the function and the value are present.
After seeing the Maybe Applicative, you may be wondering in what situation we can have a function argument that may or may not be there. We are going to see how we can use this idea to perform data validation operations in the second part of this series.
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.