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 type includes a
Name and an
data Person = Person Name Address deriving (Eq, Show)
Address types are both wrappers around the
newtype Name = Name String deriving (Eq, Show) newtype Address = Address String deriving (Eq, Show)
Before creating a
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
Just are the two data constructors of the
Maybe data type.
validateLength alternatively returns a
Just String or a
Nothing, we must handle the cases of both valid and invalid lengths when making a
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
Address take a
String and return a
Address data type. What then is this
fmap and how does it work with the
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
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
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
mkAddress with the
Person data constructor, since it expects a
Name and an
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?
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
Putting all the pieces together, we have
mkPerson :: String -> String -> Maybe Person mkPerson n a = (fmap Person (mkName n)) <*> mkAddress a
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.