Let’s think about things that might or might not happen.
data Perhaps a = Yeah a | Nah
Perhaps is a datatype that we can also use as a container for other data (by holding it inside a nice Yeah
), or to show that we have no data with Nah
.
Let’s put things in it.
john :: Perhaps String
= Yeah "John" john
Great job. John is having a nice time there. How might we express an absence of John?
nope :: Perhaps String
= Nah nope
OK. No Johns here.
So what if we have a function that receives something wrapped in a Perhaps
and wants to do stuff with it.
I guess we have pattern matching, so we can use this to pull stuff out and do things with it. Let’s add some question marks to this name, because that is always a valuable thing to do.
questionAdd :: Perhaps String -> Perhaps String
Nah = Nah
questionAdd Yeah name) = Yeah (name ++ "???")
questionAdd (-- questionAdd Nah = Nah
-- questionAdd john = Yeah "John???"
Ok. Seems good. What about adding a simple exclaimation point instead? (Remembering of course that anybody more than one at any point is a sure sign of insanity.)
exclaimAdd :: Perhaps String -> Perhaps String
Nah = Nah
exclaimAdd Yeah name) = Yeah (name ++ "!")
exclaimAdd (-- exclaimAdd Nah = Nah
-- exclaimAdd john = Yeah "John!"
Ok. Sure. So far, so functional. Feels like we’re repeating ourselves though. What if we could abstract out away the unwrapping-and-then-wrapping-again and the function itself? Well sure we can! That typeclass is called Functor
. Let’s open ghci and take a look.
Prelude> :i Functor
Here’s the definition:
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
{-# MINIMAL fmap #-}
Looks like we can implement it by just using fmap
. And that type signature kind of looks like what we want (if we squint very hard indeed). It’s saying take a function of any a
to any b
(ie, (a -> b)
) and then pass me a functor with an a
in it (ie, f a
) and I will return the same functor but with the b
from your function in it (ie, f b
). So basically fmap
is the “unwrapping-and-then-wrapping-again” we talked about earlier. Sounds good. Let’s implement it.
instance Functor Perhaps where
fmap _ Nah = Nah
fmap f (Yeah a) = Yeah (f a)
Looks great. The first thing you’ll notice is that the fmap
for Nah
doesn’t do anything. That’s because although our Yeah
can contains some a
, our Nah
contains nothing at all so cares very little for our a -> b
function. However, look! The second line is unwrapping the a
from the Yeah
, and then making a new Yeah b
(because we make a b
by running f
, which is our a -> b
function). Therefore we can throw any old function at this and we should have a Nice Time.
This function for instance, is used on a String
to make the caller look somewhat unhinged.
exclaim :: String -> String
= str ++ "!!!!!!!!!!!!"
exclaim str -- exclaim "Horse" == "Horse!!!!!!!!!!!!"
But using our new Functor
instance means we can run it on our poor friend john
from earlier, even though he is wrapped up in all that Yeah
.
veryJohn :: Perhaps String
= fmap exclaim john
veryJohn -- veryJohn == Yeah "John!!!!!!!!!!!!"
Sorry John.
What if we run the same function over a Nah
? Does everything explode?
stillNope :: Perhaps String
= fmap exclaim nope
stillNope -- stillNope = Nah
Nah. Seems fine. Nah
ignores the function altogether, as it has no a
in it, so no interest.
Ok. So this seems pretty great. There is one thing to know about Functor
however that can be a little bit brain bending at first, and that’s the idea that they can be “lawful”. That is to say, that when you fmap
over something, it doesn’t also break that thing, and that it unwraps and wraps in the right way. Haskell’s type system can help you make sure your fmap
has the right types, but it can’t enforce that your Functor
makes sense I’m afraid. That’s up to you.
The first law is called Identity. It means that if you fmap
using an Identity function, nothing will change. This is a trick I suppose - the Identity function looks like this:
identity :: a -> a
= a
identity a -- this is also called id in the Prelude
So running it on anything does nothing - the Identity law is basically checking the Functor
is up to no funny business. Our Perhaps functor is OK, but what about this chancer?
data Poohoops a = Yerp a | Nerp deriving (Eq, Show)
instance Functor Poohoops where
fmap _ Nerp = Nerp
fmap f (Yeah a) = Nerp
If we fmap identity Nerp
we get Nerp
, so that all seems fine. However, if we fmap identity Yeah "Detroit"
then we also get Nerp
which is an absolute bloody disaster. This functor is broken. Put it straight in the bin, and set the bin on fire.
The second law is called Composition. It means that if we fmap
one function over our Functor
and then fmap
a second function over the result, it would be the same as combining the two functions and doing a single fmap
.
I’m sorry. What?
OK, let’s have an example of that.
Therefore, if as well as our rather worrying “exclaim” function, we have one for shouting as well (don’t be distracted by the <$>
for now, we’ll come to it in future…)
capitalise :: String -> String
= toUpper <$> str
capitalise str -- capitalise "Horse" == "HORSE"
…we can combine it to make one terrifying function…
shouting :: Perhaps String -> PerhapsString
= fmap (capitalise . exclaim) p
shouting p -- shouting (Yeah "Bruce") == Yeah "BRUCE!!!!!!!!!!!!"
…which is exactly the same as this:
shouting2 :: Perhaps String -> PerhapsString
= fmap capitalise (fmap exclaim p)
shouting2 p -- shouting2 (Yeah "Bruce") == Yeah "BRUCE!!!!!!!!!!!!"
The Composition law just makes sure these are the same thing, so again, no funny business can take place.
compositionLaw :: Bool
= fmap (capitalise . exclaim) (Yeah "Bruce")
compositionLaw == fmap capitalise (fmap exclaim (Yeah "Bruce"))
-- compositionLaw == True
-- ie, either way of doing this ends up the same
Anyway. This is just one introduction to a kind of functor
. In short, hand-wavey terms:
“A functor is a thing that lets you safely crap around with the values inside it without breaking the thing itself”
A few important notes and disclaimers:
There doesn’t have to be just one
a
in the functor for this pattern to work. Another goto example for this isList
, which can have absolutely loads in. In that case, thefmap
runs the(a -> b)
function on every item in the list, likearray.map
from Javascript, so it takes[a]
and turns it into[b]
.There are functor instances for many datatypes such as
IO
where thea
inside might represents a value that isn’t there yet (like perhaps it will come from some user input etc). Therefore doingfmap
on such a datatype is just saying “change thea
inside tob
whenever it happens to turn up”, like “when the user types their name, change the string to have lots of exclamation marks so they look like a weirdo when we later print it back for them” or similar. This kind of idea can get a bit brain bending but once it settles it’s sort of magical to know you can crap around with the future just like you’re working with an array.
- The
Perhaps
data type we have invented is really calledMaybe
and it’s all over the place. More on that another time, perhaps.
That’s quite enough for now.
Make sense? If not, why not get in touch?
Further reading: