Constructors are Functions
Remember how constructors are really just functions? For example:
> data Person = Person String Int deriving Show
> :t Person
Person :: String -> Int -> Person
When we ask GHCi to tell us the type of any constructor, it informs us that the constructor is a function which takes however many arguments the constructor takes and outputs a value of whatever the type is.
Person
is a type and it has a single constructor, also named Person
. It takes two arguments, a String
and an Int
.
What is the type of Person
? It is a function which takes a String
and an Int
and outputs a Person
.
The same applies for any constructor.
> data Car = Car String String Int deriving Show
> :t Car
Car :: String -> String -> Int -> Car
The Car
type has a single constructor which takes a String
(for the make), a String
(for the model) and an Int
(for the year).
The type of Car
is String -> String -> Int -> Car
.
Ok, so constructors are functions. We mentioned in previous lessons that this fact would become useful to us later. Now that we've been introduced to the Applicative
type class, we're in a good position to see one of the benefits of the fact that constructors are functions as well as a practical and common use case for Applicative
.
String
and an Int
but I don't have those!
To make a Person, I need a Imagine a very simple scenario: you're writing a web application and need to process some input from the user that is submitted via a form. It is possible that some fields of our form have been left blank. As such, when we process the form in our application, we've decided to represent this possible failure using the Optional
type. If the field was filled out, we get a Some
and if it's empty we'll get a None
.
If we want to construct a Person
using text submitted via a form, this means we'll end up with Optional String
and Optional Int
. How are we supposed to create a Person
from this? The Person
constructor is a function, but it has this type signature:
String -> Int -> Person
That doesn't help us, since we don't have String
and Int
, but instead Optional String
and Optional Int
.
You should realize right away that there's simply no way to materialize a Person
if all we have is an optional String
and an optional Int
. But we can definitely output an Optional Person
!
In fact, we can write a function that does this without too much trouble. Let's try:
maybePerson :: Optional String -> Optional Int -> Optional Person
maybePerson name age = case name of
Some n -> case age of
Some a -> Some (Person n a)
_ -> None
_ -> None
Here we use a case expression to output Some Person
if the name
and age
supplied are both Some
s. If either of them is a None
, we output a None
.
We could also write it using pattern matching:
maybePerson :: Optional String -> Optional Int -> Optional Person
maybePerson (Some age) (Some name) = Some (Person age name)
maybePerson _ _ = None
Does this work? Let's see:
> let name = Some "Bob"
> let age = Some 42
> maybePerson name age
Some (Person "Bob" 42)
And if we have None
for one of the values
> let name = Some "Bob"
> let age = None
> maybePerson name age
None
or both of them
> let name = None
> let age = None
> maybePerson name age
None
We get None
.
Has to be a better way
Great, so we know we can write a function that help us construct a Person
-- or at least an Optional Person
if instead of having a String
and Int
at our disposal, we only have an Optional
String
and Int
.
But implementing this functionality the way we just did is tedious, error-prone and, most of all, verbose. Also, Optional
isn't the only kind of container or context we might have and we don't want to have to write this type of function for each possible context!
Let's back up and see what it is we are really looking for.
As we've mentioned, ordinarily we're looking to construct a Person
using a plain String
and Int
. In other words, the constructor function works just fine since it's signature is:
String -> Int -> Person
But we need something that looks like this:
Optional String -> Optional Int -> Optional Person
Well, let's express this situation as a function. We have String -> Int -> Person
but we want Optional String -> Optional Int -> Optional Person
. So we need a way to get what we want given what we have. In other words, we need a function that looks like this:
(String -> Int -> Person) -> (Optional String -> Optional Int -> Optional Person)
See why? The first argument is the signature for the Person
constructor, which we have, but we need to add the Optional
type constructor to each of those types.
Let's look at this even more generaly and forget about the particulars of Optional
. We'll think just in terms of type constructors so we'll reframe the problem as this:
(String -> Int -> Person) -> (f String -> f Int -> f Person)
instead of Optional
we now just have any type constructor f
.
Let's generalize this even further!
(a -> b -> c) -> (f a -> f b -> f c)
There's no reason we need to think about the Person
type specifically so we'll just think in terms of the type variables a
, b
and c
.
The Shortcomings of Functor
Let's look again at the signature of the function we need:
(a -> b -> c) -> (f a -> f b -> f c)
Does this signature seem somewhat familiar? If it does, it's because it looks very similar to the signature of the map
function from the Functor
type class.
We want this:
(a -> b -> c) -> (f a -> f b -> f c)
And Functor
's map
function is:
(a -> b) -> (f a -> f b)
The function we're looking for is nearly identical to map
, except that the function we'd like has an extra argument. What can we do?
Well let's just try writing this function ourselves. We'll call it map2
since, unlike the regular map
, the first argument here will be a function that takes two arguments instead of one ((a -> b -> c)
instead of (a -> b)
).
We're going to try to use the existing map
function in implementing this. In order to do that, we need a constraint on this function which is that we'll require f
to be a Functor
. You should recall that this is how we do constrained polymorphism in Haskell.
Ok, so our type declaration is this:
map2 :: Functor f => (a -> b -> c) -> (f a -> f b -> f c)
map2 f fa fb = undefined -- what do we do here??
You can try implementing this function..it's actually a really good exercise. After some time, though, you'll conclude, rightfully so, that it just isn't possible to implement this with the power of Functor
's map
alone. It just doesn't work. Why exactly? Let's follow the types:
the f
binding in our map2
function has the type a
to b
to c
.
f :: a -> (b -> c)
What happens if we use map
with f
? Here we just want to know what the type will be if call map
with f
and omit map
's second argument. Remember, this is called partial application. There's nothing wrong with examining the type at each step in function application.
Well let's remember what map
does. All it does is lift a function from being a function a -> b
to a function f a -> f b
. So here that means that means that map f
has this type:
map f :: f a -> f (b -> c)
So far so good. The second argument for our map2
function is an f a
so let's provide that as the second argument in our application of the map
function. What do we get?
map f fa :: f (b -> c)
At this point we've "used" the f
argument, we've used the f a
argument. The result we're left with is that we still have f b
to use somehow and the result of using map
with f
and f a
is a type of f (b -> c)
.
To summarize, we're left with
Using map
with our first two arguments gives us f (b -> c)
and we still have that f b
argument just dangling there.
You can try all you want, but map
does not allow us to implement map2
.
Applicative to the Rescue
So Functor
fails us and we're back to square one. Our original goal was a pretty simple one. We wanted to construct a Person
but we only had an Optional String
and an Optional Int
. We quickly realized that we needed was a function that could do this:
(a -> b -> c) -> (f a -> f b -> f c)
We tried using map
but it didn't work because we were left with
f (b -> c)
and an f b
and there's nothing map
can do to help us with that.
You may realize by now that Applicative
is precisely what we need! Applicative
provides the power that Functor
fails to give us. Look at "apply" from Applicative
. The whole point of it is to do exactly what we've been looking for.
(<*>) :: f (a -> b) -> f a -> f b
Given a function inside of a context and a value inside of some context, apply the function to the value.
So the dead-end we arrive at with Functor
is exactly where Applicative
picks up. Let's use this then to implement what we wanted!
First, we know that the constraint on our function needs to be Applicative
now. We initially thought the only constraint we needed was Functor
, but it's clear now that Functor
doesn't provide enough power and we need to resort to Applicative
to accomplish our goal.
So we'll first change the signature of map2
to reflect this:
map2 :: Applicative f => (a -> b -> c) -> (f a -> f b -> f c)
Now we can implement this:
map2 :: Functor f => (a -> b -> c) -> (f a -> f b -> f c)
map2 f fa fb = map f fa <*> fb
Let's look at this implementation
map f fa <*> fb
The first part is map f fa
. The type of this is f (b -> c)
. We can now use "apply" from Applicative
to finish this off since "apply" says that if you have a function in a context, I can apply it for you to a value in a context. Well, we do have a value in a context, that's f b
. That's all there is left to do, use "apply" with f b
:
map f fa <*> fb
In other words, we have map f fa
which reduces to
f (b -> c)
and we have
f b
.
"Apply" is precisely for this situation:
(<*>) :: f (a -> b) -> f a -> f b
Let's see what we can do with this now:
> map2 Person name age
Some (Person "Bob" 42)
> map2 Person name None
None
> map2 Person None (Some 42)
None
If we wanted to do something similar when constructing a Car
, we'd need a map3
function, since the Car
constructor takes three arguments. We can implement this using the same exact pattern we used to implement map2
:
map3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
map3 f fa fb fc = ((map f fa) <*> fb) <*> fc
Let's try it out:
> let make = Some "honda"; model = Some "accord"; year = Some 2016
> map3 Car make model year
Some (Car "honda" "accord" 2016)
These functions map2
and map3
that we've implemented are actually included in the standard library. In the Prelude, they are called liftA2
and liftA3
respectively.
Pretty it Up
There are cases where using liftA2
and liftA3
makes sense, but in most cases, using the pattern of combining map
with <*>
directly is better, such as when we did:
> map Person name <*> age
Some (Person "Bob" 42)
But there's a simple thing we can do to make this prettier and it's what is done in the Prelude. We can simply add an infix version of map
from Functor
.
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = map
With that, we can now do:
> Car <$> make <*> model <*> year
Some (Car "honda" "accord" 2016)
Or:
> Person <$> name <*> age
Some (Person "Bob" 42)
Much nicer.
In this lesson we took a closer look at the motivations behind Applicative
and a few cases that showcase the tremendous power and usefulness of this simple type class.