Functors, Applicatives, and Monads: You don't need to know theory to use them
Figuring out how to use the common functional programming typeclassess is not as hard as you would think. The key here is not to start with understanding the category theory behind them, but rather first start with using their implementations in the standard datatypes. Eventually by using them throughout different datatypes you will be able to grasp the larger picture, without having to touch the slightest bit of category theory.
What datatypes that implement any of Functors, Applicatives or Monads have in common is that they all have some data, usually polymorphic (aka it really could be any type of data) locked inside, and that data can’t be naively accessed. Functors, Applicatives, and Monads each define certain specific ways that that data can be accessed. Throughout this article I will use standard datatypes that are easy to reason about, and show how their implementations of the aforementioned three typeclasses pan out.
One easy to reason about datatype in Haskell is the datatype Maybe a
, in this case specified to be Maybe String
. This datatype can either be constructed as Just String
, which is simply a wrapper around a string or Nothing
, which doesn’t hold any value at all. With this in mind you can see that if we receive a Maybe String
with no guarantees on whether or not it actually contains a string or not, there is no simple way to access the enclosed string. For example suppose there was a function
getInnerString :: Maybe String -> String
Just string) = string
getInnerString (Nothing) = error "system blowing up, activating emergency protocols..." getInnerString (
and we gave it a Nothing
. In that case it would only have the option of crashing the program (with an error), and that is something we do not want to do.
The most basic typeclass available to us is the Functor, and it provides us with the ability to apply a function to the value inside the datatype that implements the Functor without worrying about whether or not it is possible to unwrap it. The important function that is required to implement the Functor typeclass is
(<$>) :: Functor f => (a -> b) -> f a -> f b
This may seem confusing, especially since haskellers like to use these sorts of symbol functions between the two arguments that they are applying to it, but specifying it to Maybe String
and renaming it to something pronounceable already helps to remove almost all of the confusion
map :: (String -> b) -> Maybe String -> Maybe b
Here instead of unwrapping Maybe and then applying a function to the unwrapped value, we instead provide Maybe with the function that we want to apply, and let it take care of the rest. One result of having Maybe apply the value is that the resulting value has never gotten unwrapped, and therefore we need not worry about wrapping it up again.
Here are some examples of how this plays out
= (<$>)
mapF
smellsBadF :: String -> String
= who ++ " smells awful"
smellsBadF who
maybeJamesF :: Maybe String
= Just "James"
maybeJamesF
maybeJohnF :: Maybe String
= Nothing
maybeJohnF
maybeStinkyJamesF :: Maybe String
= mapF smellsBadF maybeJamesF
maybeStinkyJamesF -- > maybeStinkyJamesF
-- Just "James smells awful"
maybeStinkyJohnF :: Maybe String
= mapF smellsBadF maybeJohnF
maybeStinkyJohnF -- > maybeStinkyJohnF
-- Nothing
You may have used this even if you have never seen a ‘functional language’ before. It is exactly the same as the map function several languages use over a linked list. If you think of it, it makes sense that linked lists also implements the Functor typeclass. After all it is also a container that includes data, but whose data is also not easily accessible. For example, even if we assume the head of the list is all the so contained data, we can’t even be sure that the list we are getting has a head, or instead is an empty list
= (<$>)
mapF
smellsBadF :: String -> String
= who ++ " smells awful"
smellsBadF who
peopleF :: [String]
= ["Fred", "Sandra", "Bill"]
peopleF
ghostsF :: [String]
= []
ghostsF
stinkyPeopleF :: [String]
= mapF smellsBadF peopleF
stinkyPeopleF -- > stinkyPeopleF
-- ["Fred smells awful", "Sandra smells awful", "Bill smells awful"]
stinkyGhostsF :: [String]
= mapF smellsBadF ghostsF
stinkyGhostsF -- > stinkyGhostsF
-- []
Above using the Functor typeclass, we have discovered that even though it has a scary name, it’s bark is certainly bigger than it’s bite. We shall find below that learning the Functor typeclass was not a singular exception, but rather a general rule.
The next typeclass we shall tackle is the Applicative typeclass. Again any datatypes that implement the Applicative typeclass usually contains enclosed data that is hard to easily access. Another thing to keep in mind, is that any datatype that implements the Applicative typeclass also implements the Functor typeclass. The two functions that are required to implement the Applicative typeclass are
pure :: Applicative f => a -> f a
--and
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Again this looks more complicated then it really is, but that’s just because it is using symbols and is generic across all datatypes that implement the Applicative typeclass. If we specify to Maybe String
and change the function names, it quickly becomes much easier to reason about.
wrap :: a -> Mabye a
apply :: Maybe (String -> b) -> Maybe String -> Maybe b
For the wrap function there is really almost nothing to it. It takes a value and encloses it in the datatype that implements the Applicative typeclass. For example looking back at our maybeJames function from above we can change it just so
= pure
wrapA
-- the orginal function
maybeJamesF :: Maybe String
= Just "James"
maybeJamesF -- > maybeJamesF
-- Just "James"
-- and now using the Applicative typeclass
maybeJamesA :: Maybe String
= wrapA "James"
maybeJamesA -- > maybeJamesF
-- Just "James"
-- > maybeJamesA == maybeJamesF
-- True
The apply function is equally easy to wrap your mind around. It is also much more powerful than the mapping function from before, because whereas the map function can only apply a function to operate on the datatype’s inner value, the apply function (paired with the wrap function) can not only be used the same way, but can also be used to compose multiple wrapped values together. To see this in action we will first take the example that we worked on with the Functor typeclass and use functions from the Applicative typeclass instead.
= pure
wrapA = (<*>)
applyA
smellsBadA :: String -> String
= who ++ " smells awful"
smellsBadA who
maybeSmellsBadA :: Maybe (String -> String)
= wrapA smellsBadA
maybeSmellsBadA
maybeJamesA :: Maybe String
= wrapA "James"
maybeJamesA
maybeJohnA :: Maybe String
= Nothing
maybeJohnA
maybeStinkyJamesA :: Maybe String
= applyA maybeSmellsBadA maybeJamesA
maybeStinkyJamesA -- > maybeStinkyJamesA
-- Just "James smells awful"
maybeStinkyJohnA :: Maybe String
= applyA maybeSmellsBadA maybeJohnA
maybeStinkyJohnA -- > maybeStinkyJohnA
-- Nothing
And then we will spice it up a little by still using some of the function from above, but now also using the apply function to compose two Maybe values.
= pure
wrapA = (<*>)
applyA
smellsBadA :: String -> String
= who ++ " smells awful"
smellsBadA who
maybeFredA :: Maybe String
= wrapA "Fred"
maybeFredA
maybeJamesA :: Maybe String
= wrapA "James"
maybeJamesA
maybeJohnA :: Maybe String
= Nothing
maybeJohnA
bothSmellBadA :: String -> String -> String
=
bothSmellBadA who1 who2 ++ ", but " ++ (smellsBadA who2) ++ " too!"
(smellsBadA who1)
maybeBothSmellBadA :: Maybe (String -> String -> String)
= wrapA bothSmellBadA
maybeBothSmellBadA
maybeJamesFredBothSmellA :: Maybe String
= applyA (applyA maybeBothSmellBadA maybeJamesA) maybeFredA
maybeJamesFredBothSmellA -- > maybeJamesFredBothSmellA
-- Just "James smells awful, but Fred smells awful too!"
maybeJamesJohnBothSmellA :: Maybe String
= applyA (applyA maybeBothSmellBadA maybeJamesA) maybeJohnA
maybeJamesJohnBothSmellA -- > maybeJamesJohnBothSmellA
-- Nothing
How come we can apply only one wrapped function to two different wrapped values? The key here is that Haskell uses currying, so a function that takes two arguments, is in fact the same as a function that takes one argument and returns as a a value another function that also takes a value and then that finally returns the actual result. To see it in types
maybeBothSmellBadA :: Maybe (String -> (String -> String))
-- this is the same as Maybe (String -> String -> String) but the parens are
-- added to increase understanding
applyA1st :: Maybe (String -> (String -> String)) -> Maybe String -> Maybe (String -> String)
= applyA
applyA1st
maybeJamesFredBothSmellA1st :: Maybe (String -> String)
= apply1st maybeBothSmellBadA maybeJamesA
maybeJamesFredBothSmellA1st
applyA2cnd :: Maybe (String -> String) -> Maybe String -> Maybe String
= applyA
applyA2cnd
maybeJamesFredBothSmellA2cnd :: Maybe String
= apply2cnd maybeJamesFredBothSmell1st maybeFredA maybeJamesFredBothSmellA2cnd
Another way to see the power of Applicative’s apply function is to use Control.Monad.Writer
. It is a datatype that allows us to have a value that comes with an accompanying log. When the writer’s are composed, the logs are composed (by appending) for free too.
import Control.Monad.Writer
= (<*>)
applyA = pure
wrapA
jamesWriterA :: Writer [String] String
= writer ("James", ["Creating Person: James"])
jamesWriterA
fredWriterA :: Writer [String] String
= writer ("Fred", ["Creating Person: Fred"])
fredWriterA
theyHateA :: String -> String -> String
= person1 ++ " hates " ++ person2
theyHateA person1 person2
theyHateWriterA :: Writer [String] (String -> String -> String)
= wrapA theyHateA
theyHateWriterA
jamesHatesFredWriterA :: Writer [String] String
= applyA (applyA theyHateWriterA jamesWriterA) fredWriterA
jamesHatesFredWriterA -- > runWriter jamesHatesFredWriterA
-- ("James hates Fred",["Creating Person: James","Creating Person: Fred"])
And that’s already most of it. The final typeclass we want to talk about is the “dreaded” Monad. But just as the Applicative and Functor typeclasses are not that hard, the Monad typeclass is not substantively harder to understand than they were either. Just like all datatypes that implement the Applicative typeclass also implement the Functor typeclass, so all datatypes that implement the Monad typeclass also implement the Applicative typeclass (and therefore the Functor typeclass too).
There is only one1 new function that needs to be declared to implement the Monad typeclass.
(=<<) :: Monad m => (a -> m b) -> m a -> m b
Again, scarier than it actually is, since it’s all symbols, and is generic for any Monad. Desymbolizing it, specifying it for Maybe String
and giving it a pronounceable name gives us
bind :: (String -> Maybe b) -> Mabye String -> Maybe b
Re-implementing what we just did with Functor and then Applicative in Monad is not too hard to manage.
= return
wrapM -- pure in the Applicative typeclass is called return, groovy right?
= (=<<)
bindM
smellsBadM :: String -> String
= who ++ " smells awful"
smellsBadM who
--instead of wrapping the whole function like with the applicative version we
--path the argument through and then wrap the result
maybeSmellsBadM :: String -> Maybe String
= wrapM (smellsBadM who)
maybeSmellsBadM who
maybeJamesM :: Maybe String
= wrapM "James"
maybeJamesM
maybeJohnM :: Maybe String
= Nothing
maybeJohnM
maybeStinkyJamesM :: Maybe String
= bindM maybeSmellsBadM maybeJamesM
maybeStinkyJamesM -- > maybeStinkyJamesM
-- Just "James smells awful"
maybeStinkyJohnM :: Maybe String
= bindM maybeSmellsBadM maybeJohnM
maybeStinkyJohnM -- > maybeStinkyJohnM
-- Nothing
With a little bit of work we can get our composing values example from earlier working with the functions available from the Monad typeclass too
= (=<<)
bindM = return
wrapM
smellsBadM :: String -> String
= who ++ " smells awful"
smellsBadM who
maybeJamesM :: Maybe String
= wrapM "James"
maybeJamesM
maybeFredM :: Maybe String
= wrapM "Fred"
maybeFredM
maybeJohnM :: Maybe String
= Nothing
maybeJohnM
maybeBothSmellBadM :: String -> String -> Maybe String
=
maybeBothSmellBadM who1 who2 ++ ", but " ++ (smellsBadM who2) ++ " too!")
wrapM ((smellsBadM who1)
maybeBothSmellBadM2 :: Maybe String -> String -> Maybe String
= bindM (maybeBothSmellBadM who1) who2
maybeBothSmellBadM2 who2 who1
maybeJamesFredBothSmellM :: Maybe String
= bindM (maybeBothSmellBadM2 maybeJamesM) maybeFredM
maybeJamesFredBothSmellM -- > maybeJamesFredBothSmellM
-- Just "Fred smells awful, but James smells awful too!"
maybeJamesJohnBothSmellM :: Maybe String
= bindM (maybeBothSmellBadM2 maybeJamesM) maybeJohnM
maybeJamesJohnBothSmellM -- > maybeJamesJohnBothSmellM
-- Nothing
Notice that in this example the ordering is opposite from what we found from using Applicative’s apply. With apply we have two Maybe values, one of which is a function, and compose them together going left to right. With Monad’s bind, this is no longer the case, but rather we have a function that takes the unwrapped value from the second Maybe value and returns another Maybe value from it, therefore making the order right to left.
With the move from the Functor typeclass to the Applicative typeclass we moved from being able to modify the value wrapped inside the datatype that implements the Functor typeclass to the ability to compose different separately wrapped elements in a datatype that implements the Applicative typeclasss together, so when we make the jump from Applicative to Monad, what new ability do we get? From comparing the types between bind and apply we can see that for the first time we no longer have our functions resulting value prewrapped for us. This comes in useful if the function we want to apply to the unwrapped value, itself already produces a wrapped value. If we had that scenario with only an Applicative we would have no way to avoid nesting. In fact one cool benefit of Monad’s bind is that if we are given an already nested structure (for example if apply was used when bind should have been used), we can flatten it by just using the bind and identity functions.
= (=<<)
bindM = return
wrapM
identityM :: Maybe String -> Maybe String
= value
identityM value
maybeJamesM :: Maybe String
= wrapM "James"
maybeJamesM
nestedMaybeJamesM :: Maybe (Maybe String)
= wrapM maybeJamesM
nestedMaybeJamesM
unNestedMaybeJamesM :: Maybe String
= bindM identityM nestedMaybeJamesM
unNestedMaybeJamesM -- > unNestedMaybeJamesM
-- Just "James"
-- > unNestedMaybeJamesM == maybeJamesM
-- True
A practical use of this again shows up when using linked lists, bind even appears for this purpose in some other languages under the guise of flatMap, a function identical to bind specified to Lists. For example suppose I wanted to map a function over a list of values, but instead of just returning one value for each, I want to return several. One option to take care of this problem is to return a lists of lists, but this quickly becomes cumbersome when going several levels deep. Instead I can use Monad’s bind to accumulate all the results in one (non-nested) list. Here we use bind to generate backronyms
import Data.List
import Data.Char
= (=<<)
bindM
-- Using a List for convenience for my (very short) word dictonary
wordDictonaryM :: [String]
= ["Resistance", "External", "Frog-legs", "Combined", "Load-",
wordDictonaryM "Understated", "Bearing", "Operational", "Treadmill"]
-- String is just a List of Char [Char]
backronymM :: String
-- Stephen
= "Colbert"
backronymM
findWordM :: Char -> String
-- Don't let the anonymous function scare you. They are functions that don't have
-- any name -- which makes them useful when they are short, and you only need to
-- use them once. In this case this anonymous function only has one argument "x"
= case find (\x -> head x == toUpper c) wordDictonaryM of
findWordM c Just x -> x ++ " "
Nothing -> c : " "
aTreadmillM :: String
= bindM findWordM backronymM
aTreadmillM -- > aTreadmillM
-- "Combined Operational Load- Bearing External Resistance Treadmill "
And that’s really all there is too these scary typeclasses. Just like in Java where you have some Objects where it’s not possible to directly change the inner state, in Haskell you have some datatypes that also don’t allow directly changing their inner variable(s). Just like in Java where if we know what class the Object inherits from we can use certain generic functions on them, in Haskell we have typeclasses, and if a datatype implements a certain typeclass, we can be sure there will be certain functions that will work on it.