For the past few months, I have been reading through Haskell Programming from First Principles. It is a wonderful book, worth reading carefully with many opportunities to develop a well-grounded understanding of Haskell, a challenging language with a wealth of powerful ideas.
This post will not cover any basics of installing Haskell and so on. Instead, look here for more on getting the language running on your machine. Also, if you're entirely new to Haskell and prefer working with free materials, see here for a great way to get started learning the language.
Recently, while reading the chapter on applicatives I encountered a practical example of how one might use applicatives with functors. While it might sound too abstract, the example below is actually readily understandable, especially if we proceed by thinking first and foremost about types and type signatures.
As a disclaimer, the example we will discuss comes straight out of Haskell Programming. This post is an attempt at reinforcing my own understanding of the example. Needless to say, an interested reader will read the chapters on functors and applicatives for the full context. In addition, there is also this incredible post on functors and applicatives which includes plenty of helpful visual guides as well as a nice introduction to the concepts.
Now for the example. Let's assume we have a type which represents a Person
. The Person
type includes a Name
and an Address
:
data Person =
Person Name Address
deriving (Eq, Show)
The Name
and Address
types are both wrappers around the String
type:
newtype Name =
Name String
deriving (Eq, Show)
newtype Address =
Address String
deriving (Eq, Show)
Before creating a Name
or Address
type, we will ensure the provided String
type satisfies a validation check: for names we will require no more than twenty-five characters. For addresses, we will require no more than 100 characters.
To handle validation, we will use a function which accepts a character limit (the Int
type) and a string (the String
type) which represents the name or the address.
validateLength :: Int -> String -> Maybe String
validateLength maxLen s =
if (length s) > maxLen
then Nothing
else Just s
What is interesting about this validation function is that it returns a Maybe
type. In other words, the validation can fail and when it does, its return value will be a Nothing
. Successful validation will result in the value wrapped in a Just
type. Both Nothing
and Just
are the two data constructors of the Maybe
data type.
Because validateLength
alternatively returns a Just String
or a Nothing
, we must handle the cases of both valid and invalid lengths when making a Name
or Address
. To do that, we will use fmap
, a function of the Functor
type class.
First, let's start with the two make functions:
mkName :: String -> Maybe Name
mkName s = fmap Name $ validateLength 25 s
mkAddress :: String -> Maybe Address
mkAddress a = fmap Address $ validateLength 100 a
Both the data contructors Name
and Address
take a String
and return a Name
or Address
data type. What then is this fmap
and how does it work with the Maybe
type?
Let's look at the fmap
function on its own for a moment starting with its type signature.
fmap :: Functor f => (a -> b) -> f a -> f b
The fmap
function applies a function to a value within a Functor
and returns a new Functor
with the transformed value.
Now, if we consider the types involved in mkName
for example, we'll see that fmap
does exactly what we need:
-- assuming some input
fmap Name (validateLength 25 "some-input")
-- we have these types
fmap (String -> Name) (Maybe String)
-- which match the signature of fmap
fmap (a -> b) -> f a
From the types above, we can see that fmap
will apply a function to the value within a Functor
(Maybe
in the case here) and then return a new Functor
(again a Maybe
type) with the transformed value within it (a Maybe Name
type). In Haskell parlance, we are lifting a function over structure.
So now that we have two mk
functions and understand their workings, we have a problem: how will we use mkName
and mkAddress
with the Person
data constructor, since it expects a Name
and an Address
object?
If we write a mk
function for our Person
type, it will need two string arguments:
mkPerson :: String -> String -> Maybe Person
mkPerson = undefined -- <- what goes here?
Both mkName
and mkAddress
return a Maybe Name
or a Maybe Address
. How can we use all three mk
functions together?
The answer is to use an applicative. To understand why, let's look at the type signature for the applicative operator <*>
:
-- :type (<*>)
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
The type signature for <*>
looks similar to fmap
above with the exception that the first argument -- a function -- is itself contained within an applicative structure. That wrapped function is then applied to the second argument -- an applicative wrapping a type which matches the function's input type -- and finally the output is an applicative wrapping a type of the function's return value.
Let's construct an example matching the types to help make this clear. What if we could use the <*>
operator with the following types? Would the types satisfy the type signature of <*>
?
Maybe (Address -> Person) <*> Maybe Address
We start with a Maybe
type wrapping a function which takes an Address
and returns a Person
. The right side of the <*>
operator is a Maybe Address
. Putting this next to the type signature of <*>
, we see the types line up perfectly:
f (a -> b ) <*> f a -- returns f b
Maybe (Address -> Person) <*> Maybe Address -- returns Maybe Person
Needless to say, Maybe
is a member of the Applicative
type class.
We know that mkAddress
takes a String
and returns a Maybe Address
, which means we can use it as part of our mkPerson
function. How then do we get a Maybe (Address -> Person)
?
We can use fmap
to produce exactly this function:
fmap Person (mkName String)
If we expand the types of the function above, we have:
fmap (Name -> Address -> Person) (Maybe Name)
-- which becomes
Maybe (Address -> Person)
We have used fmap
to apply the result of mkName
to our Person
data constructor, leaving a partially applied function which now takes an Address
and returns a Person
, all inside a Maybe
structure.
Putting all the pieces together, we have
mkPerson :: String -> String -> Maybe Person
mkPerson n a = (fmap Person (mkName n)) <*> mkAddress a
Or, swapping fmap
with its infix operator <$>
:
mkPerson :: String -> String -> Maybe Person
mkPerson n a = Person <$> mkName n <*> mkAddress a
And now we have a practical example of using applicatives with functors.