Definitions, examples using Maybe and [], laws, and more.
Functors
We are familiar with map. It takes a function a -> b and a list of values [a] and produces another list of values[b]. We unwrap each value a from the list [a] and apply a -> b to it, collect all the results b, and wrap them again to produce the list [b]. Here [] is a wrapper around values and map is an operation defined based on the wrapper.
For another example, consider the wrapper Maybe. We could have a function a -> b and a Maybe a and we want to produce Maybe b. The semantic is similar. We try to unwrap a value from Maybe a:
- Sometimes we get an
abecause we have aJust a. Then we applya -> bto it, collect the resultb, and wrap it again withJust b, which is also aMaybe b. - Other times we can get nothing because we have a
Nothing. Then we simply produceNothing. Note that this producedNothing, which is aMaybe b, is different than the inputNothing, which is aMaybe a.
In either case, a Maybe b is produced.
The semantics can vary for different wrappers, but the pattern is the same. Generally, suppose we have a wrapper f. We would like to define an operation based on f, such that it takes a function a -> b and a wrapped value f a and produces another wrapped value f b. We call such an operation fmap, a generalization of map. With the definition of fmap, the wrapper f is called a functor.
1 | class Functor f where |
fmap can also be written as the infix operator <$>.
Functor laws
A proper fmap must satisfy two laws:
1 | fmap id = id |
Due to currying, fmap g has type f a -> f b, where g :: a -> b. That is, given the function g, fmap g takes a wrapped value f a and produce another wrapped value f b.
The first law states that when g :: a -> a is the identity function, fmap g should also be the identify function (with type f a -> f a).
The second law states that function composition should be preserved for any functions g :: b -> c and h :: a -> b.
Applicatives
fmap has an annoying limit. It only accepts functions with one argument. But we might need more. For example, we might need to apply + to two Maybe Int‘s. We produce Just the sum of the two Int‘s if they are all Just, and Nothing if at least one of them is Nothing, i.e. (Just 2) + (Just 3) = (Just 5) and (Just 2) + Nothing = Nothing.
If we try to use fmap on this problem, what can we get? Remembering + :: Int -> Int -> Int, and we find that fmap + (Just 2) is Just (2+). We now have a function wrapped by Just! At this stage we can do nothing with a wrapped function, because in the definition of fmap the function a -> b is unwrapped.
So we introduce an operation <*> to handle wrapped functions:
1 | (<*>) :: f (a -> b) -> f a -> f b |
We define <*> on Maybe such that, if Maybe (a -> b) and Maybe a are both Just‘s, we unwrap them, apply the function a -> b to the value a, obtain the result b, and wrap it to produce a Just b, which is Maybe b; otherwise, we simply produce a Nothing, which is also Maybe b.
And naturally, we now have (Just (2+)) <*> (Just 3) = (Just 5) and (Just (2+)) <*> Nothing = Nothing. If we expand Just (2+) and rewrite fmap as <$>, we now have
1 | (+) <$> (Just 2) <*> (Just 3) |
Looks nice, right?
To further illustrate, define a function
1 | add :: Int -> Int -> Int -> Int |
and apply
1 | add <$> (Just 2) <*> Nothing <*> (Just 3) |
Step 1, applying fmap, we get
1 | (Just (add 2)) <*> Nothing <*> (Just 3) |
where Just (add 2) has type Maybe (Int -> Int -> Int).
Step 2, we feed Nothing to a wrapped function Just (add 2) , and we get
1 | Nothing <*> (Just 3) |
where Nothing has type Maybe (Int -> Int), which is a wrapped (but vanishing) function.
Step 3, “applying” the Nothing function to Just 3 and we get Nothing, due to the definition of <*>.
So applicatives are simply a way to handle curried functions in the context of wrappers by defining <*>.
As we have seen, applicatives (in effect) can defined using <$> and <*>. However, the formal way to define them is to use pure and <*>, where
1 | pure :: a -> f a |
which is simply “wrapping”. For example, for Maybe, pure a is Just a.
It is not hard to observe that <$> is in fact a combination of pure and <*>:
1 | g <$> a = pure g <*> a |
They are the same that we produce a wrapped function at “step 1” using fmap, and that we produce a wrapped function at “step 0” by directly wrapping it using pure.
List is also an applicative, where pure is to transform a value into a singleton list, and <*> is to apply a list of functions to a list of values and produce a “Cartesian product” of results (in a flattened list). Here it is intuitive to think of a list as “all possible values” of some variable, e.g. the solution to an equation. If x has possible values [1, 2, 5] and y has possible values [10, 20], what are the possible values of x + y? The answer is simply
1 | (+) <$> [1, 2, 5] <*> [10, 20] |
or equivalently
1 | pure (+) <*> [1, 2, 5] <*> [10, 20] |
which produces [11, 21, 12, 22, 15, 25].
Applicative laws
1 | pure id <*> x = x |
Monads
Let’s review what we have now, which is just pure and <*>, as fmap can be built from them. We know that pure is to wrap, and <*> takes a wrapped function f (a -> b) and a wrapped value f a to produce another wrapped value f b. Notice that the wrapped function f (a -> b) here, once unwrapped, is just another plain, ordinary, pure function a -> b. In another words, at this stage, we only have “pure” functions (albeit possibly wrapped).
We have been using the letter f for a wrapper. We now change it to m, for reasons you can easily guess.
Consider an “effectful” function of type a -> m b, which introduces a wrapper “internally”. For example, we might want to halve an Int and produce another Int. However, this is impossible, because odd numbers cannot be halved. So we can only produce a Maybe Int. If the input Int is an odd number, we produce a Nothing. Otherwise, we produce Just the input divided by 2. The function halve thus has type Int -> Maybe Int.
1 | halve x | even x = Just (x `div` 2) |
What if we want to apply halve to an Int multiple times? The first time we do this, we get a Maybe Int. The second time we do this, we need to check whether this Maybe Int is a Just or a Nothing. If it is a Just, we proceed with the extracted Int; if it is a Nothing, we produce a Nothing and happily (or sadly?) skip all the following operations as the final result is bound to be a Nothing.
Here a recurring pattern is: given a wrapped value m a and an effectful function a -> m b, produce a wrapped value m b. We define the operator >>= to handle it:
1 | (>>=) :: m a -> (a -> m b) -> m b |
So now we can use
1 | halve 8 >>= halve >>= halve |
or equivalently
1 | pure 8 >>= halve >>= halve >>= halve |
which evaluates to Just 1.
We also have
1 | halve 6 >>= halve >>= halve |
which evaluates to Nothing.
Note that we could have written
1 | halve 6 >>= \x -> |
but \x -> halve x is simply halve.
halve is a single-argument function. Consider the following function safediv:
1 | safediv :: Int -> Int -> Maybe Int |
It takes two Int‘s and produces a Maybe Int. Suppose we want to (safely) calculate $(20 / 5) / (4 / 2)$, using >>= and lambda expressions, we write
1 | safediv 20 5 >>= \n -> |
which produces Just 2.
Using syntactic sugar, the above can be refactored as
1 | do |
A wrapper with operations >>= and pure defined is called a monad. For monads, pure is written as return with the same definition.
A list is also a monad. For a list xs :: [a] and a function g :: a -> [b], xs >>= g applied g to each of the elements in the list xs, collecting all the results values in a list. For example, define mySqrt as
1 | mySqrt :: Float -> [Float] |
Then
1 | [4, 0, -1, 9] >>= mySqrt |
evaluates to [2.0, -2.0, 0.0, 3.0, -3.0], and
1 | do |
evaluates to [1.4142135, -1.4142135, 0.0, 1.7320508, -1.7320508].
Using lists as a monad is similar to list comprehensions:
1 | pairs :: [a] -> [b] -> [(a,b)] |
while equivalently,
1 | pairs :: [a] -> [b] -> [(a,b)] |
Monad laws
1 | return x >>= f = f x |