Recently I've spent some time staring at type signatures. The goal was to develop a better intuition by absorbing their wisdom. Last week it was Monad's bind
. This time I've decided to compare the following four:
($) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
(>>=) :: (a -> m b) -> m a -> m b
Function Application or ($)
($) :: (a -> b) -> a -> b
It takes a funtion from a value of type a
to a value of type b
, an a
and returns b
. There's only one possible way to implement ($)
which is to apply the funtion to the value of type a
.
Functor's fmap
or (<$>)
(<$>) :: (a -> b) -> f a -> f b
The only difference from the previous is that a
and b
exist in a context f
. For example, we could have an Int
in a List
context (i.e. [Int]
), which means we went from one Int
to any number of Int
s. Or we could have an Int
in a Maybe
context (i.e. Maybe Int
), in other words there could be either no Int
s or just one Int
. And so on and so forth depending on the semantics of each functor.
Again, it's easy to see how the value of type a
must feed the function from a
to b
to generate the output. The only difference from ($)
is that depending on the semantics of the context f
, the function will be applied in a different way.
Applicative Functor's sequential application or (<*>)
(<*>) :: f (a -> b) -> f a -> f b
In this instance, the function from a
to b
has a context f
too. Therefore, the way the output is calculated depends on both the first and the second f
(which must be the same f
).
Monad's bind
or (>>=)
(>>=) :: (a -> m b) -> m a -> m b
This time, the way the function is applied depends only on the second m
. This is the same situation as for (<$>)
. But there's one important change: the previous functions could only transform an a
into a b
. In the case of bind
, the funtion decides not only on the b
but also on the m
, which must be the same m
for both.
Concretely
Let's see the above in action in the context of Either
which has an instance for Functor, Applicative Functor and Monad. Notice that the instances are defined for Either e
because the context they provide is around one type, not two. For example, given an Int
we can provide it an Either String
context by doing Either String Int
.
show 1
--> "1"
-- ($)
show $ 1
--> "1"
-- (<$>)
show <$> Right 1
--> Right "1"
show <$> Left "string"
--> Left "string"
-- Either maps the function only when the value is a `Right`.
-- (<*>)
Right show <*> Right 1
--> Right "1"
Right show <*> Left "string"
--> Left "string"
Left show <*> Right 1
--> Left show
Left show <*> Left "string"
--> Type error: the type on the left should be the same for both `Either`s.
-- Either applies the function only when both values are `Right`.
-- (>>=)
Right 1 >>= (\x -> Right (show x))
--> Right "1"
Left "string" >>= (\x -> Right (show x))
--> Left "string"
Right 1 >>= (\x -> Left (show x))
--> Left "string"
Left "string" >>= (\x -> Left (show x))
--> Left "string"
-- Either binds the function only when the value before `>>=` is a `Right`.
-- Contrarily to the previous cases, `>>=` can decide to return `Left` or `Right`.