When learning about the different type classes in Haskell the one I struggled the most with was, by far, Applicative.
Functor
Functor is, at least to some extent, straightforward. We take any unary functions and make them work on some (functor) context.
fmap :: Functor f => (a -> b) -> f a -> f b
-- or the infix version
(<$>) :: Functor f => (a -> b) -> f a -> f b
Say we have an increment function that works on Int
s.
inc :: Int -> Int
λ> inc 1
2
By using fmap
we can map any functor that contains an Int
.
λ> fmap inc [1, 2, 3]
[2, 3, 4]
λ> fmap inc (Just 1)
Just 2
λ> fmap inc (Right 1)
Right 2
That becomes very clear when we align fmap
with the function application operator.
By the way, we'll use the infix version from now.
Applicative
In the case of applicative it's not clear. Or at least it took longer to click for me.
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Comparing with ($)
doesn't really help. Why would I want to also have the function in the context?
($) :: (a -> b) -> a -> b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
We said functors allow to apply a unary function in a context. But what happens if I want to apply a function with a higher arity?
add :: Int -> Int -> Int
add <$> (Just 1) -- ??
What does the repl says? 🦊
λ> :t add <$> (Just 1)
add <$> (Just 1) :: Maybe (Int -> Int)
Maybe (Int -> Int)
? Yes, we saw that already in the (<*>)
signature.
add <$> (Just 1) <*> (Just 2) -- Just 3
Let's dissect that 🔍
-- refresh these ones first :)
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
-- With Maybe applied (using TypeApplications extension)
(<$>) @Maybe :: (a -> b) -> Maybe a -> Maybe b
add :: Int -> Int -> Int
-- With Int applied in place of 'a'
(<$>) @Maybe @Int :: (Int -> b) -> Maybe Int -> Maybe b
-- With (Int -> Int) applied in place of 'b'
(<$>) @Maybe @Int @(Int -> Int)
-- (a -> b) -> Maybe a -> Maybe b
:: (Int -> Int -> Int) -> Maybe Int -> Maybe (Int -> Int)
add <$> (Just 1) :: Maybe (Int -> Int)
Here we see the first interesting thing. Since our add
function takes two arguments (or to be more accurate one at a time). But we only provide one (the Int
from Maybe Int
), so it gets partially applied and returns a function (Int -> Int
). So b
is Int -> Int
.
-- a -> b
add :: Int -> (Int -> Int)
Note that parens aren't actually needed since the arrow (->
) is right associative.
That was the first part of the expression. We are missing the applicative.
(<*>) @Maybe :: Maybe (c -> d) -> Maybe c -> Maybe d
-- With Int in place of 'c'
(<*>) @Maybe @Int :: Maybe (Int -> b) -> Maybe Int -> Maybe b
-- And also Int in place of 'd'
(<*>) @Maybe @Int @Int
:: Maybe (Int -> Int) -> Maybe Int -> Maybe Int
Et voilà
add :: Int -> Int -> Int
add <$> (Just 1) :: Maybe (Int -> Int)
add <$> (Just 1) <*> (Just 2) :: Maybe Int
Functor: apply unary functions in a context.
Applicative: apply n-ary functions in a context.
This is referred as lift in Haskell.
And the whole point of applying functions in such contexts is the semantics associated with them. It might be for validation, optional values (without null
😏), lists or trees of items, running IO actions, parsers.
When the context is Maybe:
λ> add <$> (Just 1) <*> (Just 2)
Just 3
λ> add <$> Nothing <*> (Just 2)
Nothing
λ> add <$> (Just 1) <*> Nothing
Nothing
λ> add <$> Nothing <*> Nothing
Nothing
When the context is Either:
λ> add <$> (Right 1) <*> (Right 2)
Right 3
λ> add <$> (Left "err 1") <*> (Right 2)
Left "err 1"
λ> add <$> (Right 1) <*> (Left "err 2")
Left "err 2"
λ> add <$> (Left "err 1") <*> (Left "err 2")
Left "err 1"
When the context is List:
λ> add <$> [1, 2, 3] <*> [1, 2, 3]
[2,3,4,3,4,5,4,5,6]
λ> (,) <$> [1, 2, 3] <*> [1, 2, 3]
[(1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3)]
☝️ More on that on the next one.
Conclusion
When learning functional programming all these type classes might seem scary. Developing a basic intuition of their purpose and usages is a big part of the process of getting comfortable using (and understanding) them.
I know there are more implications around Functor and Applicative that I have yet to discover. But as any learning process, it takes time. I'm sure more things will become clear and start sink in as I keep going.
But that's all for today.
Happy and safe coding 🎉
Top comments (10)
Thank you for the great article! I think a few of your type declarations have
Functor a
orApplicative a
where you meanFunctor f
orApplicative f
, am I correct?Thanks!
Nice catch! I'm in the phone now, I'll fix it later.
Very good article. There are a lot of issues in many publications about Applicative, they show it with relation with Monad, or get into A from M.
Showing this just as a Functor with additional powers, possibility to apply n-unary functions is much much better approach.
Tnx.
Thanks Maciej!
There's indeed more to Applicative (as well as Functors, Monads, et al). Specially if one gets in the theoretical side. Which is useful. But as a developer all I want is to be able to have the necessary intuition to use it in practice, right?
I'm currently learning about applicatives as well, so it's cool to see this. I particularly like the insight that applicatives are about applying n-ary functions in a context, the same way functors are about applying unary functions in a context. Very helpful - thanks!
Glad it was useful :)
I really enjoy the whole series, keep it up, it's so much fun to read and learn a bit more all the time ;)
It mostly me sharing what I'm learning, so I'm really glad it helps somebody else :)
It's just in the right time, I started to learn Haskell with Advent of Code 2019, and I really like it and these posts are very useful extra sources for getting more and more in a clean format ;)
Glad to hear! 🎉
Make sure to check out A Type of Programming by @k0001. I started recently with it and is a pleasure to read.
A Type of Programming
Renzo Carbonara ・ Apr 24 '19 ・ 1 min read