- By Javier Martínez
- ·
- Posted 10 Aug 2021
Functional patterns
Welcome pythonistas! In our last video session on design patterns we focused exclusively on patterns for the object oriented paradigm (OOP). To some..
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)
fmap
function. 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, f b
.There are plenty of Functor instances defined in Haskell, but we will show the two canonical ones, List
and 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.
fmap
for List
, applies the function a -> b
to each of its elements, converting [a]
to [b]
.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 fmap
for Maybe
applies the function a -> b
to the element inside Just
or simply does nothing if the value is empty, returning a Nothing
value. If the value is Nothing
it 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)
pure
takes 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 fmap
does, but in this case, both arguments, the function and the value, are wrapped in a structure f.Let's put the type signatures of fmap
and <*>
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.
List Applicative:
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] [5]
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.
The :: [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]
Maybe Applicative:
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 Just
.
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.
Stay tuned!
Welcome pythonistas! In our last video session on design patterns we focused exclusively on patterns for the object oriented paradigm (OOP). To some..
Solving a simple problem in Elixir In this article, I am going to solve the Word Count problem from Exercism in Elixir. I'll start with a form that..
Crafting web apps using finite state machines - Part 1 Code on Gitlab
Join our newsletter for expert tips and inspirational case studies
Join our newsletter for expert tips and inspirational case studies