In this series I plan on writing about various Haskell/functional programming things that contribute towards [understanding monads][1]. I want to take a different approach than an all-in-one information overload style that I see in just about every monad tutorial I read. A lot of people apply analogy exclusively to explain what monads are or what they are used for, but I think this approach is limited as analogy itself is flawed. Instead, I’ll aim to take an approach by going through the functions.
Understanding functors is the first step towards understanding monads.
A functor in Haskell is a bit of data with something else attached to it: a type that can be mapped over. I like to think of this as a context and a value:
f a
=> f
is the context, a
is the value
Just 12
=> Maybe
is the context,
12
is the value
[27]
=> []
is the context,
27
is the value
Functors have the following typeclass:
class Functor f where
fmap :: (a -> b) -> f a -> f b
So a functor is just something which implements fmap
(which is a more general version of map
, but
functor-map).
It takes a function (a -> b)
, and lifts it into the
context of the functor f
before applying it to
a
, returning a value in the context of the original
functor: f b
.
Functors have an identity law and a composition law:
fmap id = id
fmap (p . q) = (fmap p) . (fmap q)
The identity law means that applying id
to the value of
the functor and then attaching its context is the same as applying
id
to the context and value together.
The composition law means that when lifting functions to work in a
functor’s context, it doesn’t matter if those functions are composed
before being lifted (p . q)
, or if those functions are
lifted individually and then composed
(fmap p) . (fmap q)
.
Maybe
is for things which are either Just
there, otherwise Nothing
is there. Its context is optional
values.
Here are some types:
: :t Just 3
λJust 3 :: Num a => Maybe a
: :t Just 'a'
λJust 'a' :: Maybe Char
: :t Nothing
λNothing :: Maybe a
So Maybe
is a type constructor; put a type after it to
form a concrete type: Maybe Int
. Nothing
is
always abstract though, because through polymorphism, it can act as any
type of Maybe
concrete type:
Nothing :: Maybe a
where a
can be
Int
, Char
or anything else.
The Functor instance for Maybe
looks like this:
instance Functor Maybe where
fmap _ Nothing = Nothing
fmap f (Just a) = Just (f a)
So fmap
applies a given function inside a
Just
, or returns Nothing
if
Nothing
was there already:
: fmap (+1) (Just 2)
λJust 3
: fmap (*3) (Just 4)
λJust 12
: fmap (+1) Nothing
λNothing
Lists represent the context of non-deterministic (compute all possible values) evaluation, as they can contain multiple homogeneous values:
: :t []
λ :: [t]
[]: :t ['a']
λ'a'] :: [Char]
[: :t [2, 3, 5, 7, 11]
λ2, 3, 5, 7, 11] :: Num t => [t] [
[]
is the constructor here, and the :
operator is used to prepend to it. [1, 2, 3]
is actually
syntactic sugar for 1 : 2 : 3 : []
:
: 1 : 2 : 3 : []
λ1,2,3] [
fmap
for lists is actually just implemented with
map
:
instance Functor [] where
fmap = map
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
This takes a function and applies it to each element in the list, returning a list of results:
: map (*12) [1..9]
λ12,24,36,48,60,72,84,96,108]
[: map (+1) []
λ []
Composition is a functor, because functions can have functions mapped over them as per the rules of composition. The context here is function composition, which means that logically, as long as the types line up, two functions can be “joined together” via composition, and values are the functions themselves.
: :t (+1)
λ+1) :: Num a => a -> a
(: :t head
λhead :: [a] -> a
: :t id
λid :: a -> a
fmap
for functions is implemented using the
.
function composition operator:
instance Functor ((->) r) where
fmap = (.)
r
is necessary because (->)
has a kind
of * -> * -> *
, which means that it needs two types
to turn it into a concrete type. We give it the polymorphic type
r
, and currying means that (->) r
has the
kind * -> *
:
: :k (->)
λ(->) :: * -> * -> *
: :k ((->) Int)
λ->) Int) :: * -> * ((
Function composition takes two functions f
and
g
, where the domain of f
is the codomain
(range) of g
and joins them together f . g
to
form a third function which would be identical to applying
g
and then f
to a value. In Haskell, the
domains we care about are types:
: :t (+1)
λ+1) :: Num a => a -> a
(: :t (*2)
λ*2) :: Num a => a -> a
(: :t not
λnot :: Bool -> Bool
: :t ((+1) `fmap` (*2))
λ+1) `fmap` (*2)) :: Num a => a -> a
((: ((+1) `fmap` (*2)) 2
λ5
-- Composition is not commutative
-- f . g /= g . f
: ((*2) `fmap` (+1)) 2
λ6
-- Here's what happens when we try to compose functions where the types don't
-- match
: :t (not `fmap` (*2))
λ
<interactive>:1:14:
No instance for (Num Bool) arising from a use of ‘*’
In the second argument of ‘fmap’, namely ‘(* 2)’
In the expression: (not `fmap` (* 2))
Either
is a Left
or a Right
,
where Left
and Right
can be different types.
It’s Haskell convention to put correct values in the Right
constructor, because if something is right it is correct.
: :t Left 1
λLeft 1 :: Num a => Either a b
: :t Right "abc"
λRight "abc" :: Either a [Char]
-- A function which returns True when given 2, or an error message elsewise
twoOrError :: Integer -> Either String Bool
2 = Right True
twoOrError = Left "Didn't get a 2!"
twoOrError _
: twoOrError 2
λRight True
: twoOrError 3
λLeft "Didn't get a 2!"
For Either
, fmap
is implemented in such a
way that it will apply its given function to Right
values,
but leave Left
values alone:
not :: Bool -> Bool
not True = False
not False = True
: fmap not (twoOrError 2)
λRight False
: fmap not (twoOrError 3)
λLeft "Didn't get a 2!"
Try to work out the implementation of fmap
for the
Either
typeclass as a reader exercise.
(Hint: Either
has a kind
* -> * -> *
)
Answer:
instance Functor (Either e) where
fmap _ (Left l) = Left l
fmap f (Right r) = Right (f r)