DEV Community

Cover image for Why Applicative?
Christian Gill
Christian Gill

Posted on • Edited on

Why Applicative?

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 Ints.

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.

($) ::                (a -> b) ->   a ->   b
(<$>) :: Functor f => (a -> b) -> f a -> f b

inc  $   1       --  2
inc <$> [1]      -- [2]
inc <$> (Just 1) -- Just 2
fmap is just function application inside a context.

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)

Collapse
 
ksaaskil profile image
Kimmo Sääskilahti

Thank you for the great article! I think a few of your type declarations have Functor a or Applicative a where you mean Functor f or Applicative f, am I correct?

Collapse
 
gillchristian profile image
Christian Gill • Edited

Thanks!

Nice catch! I'm in the phone now, I'll fix it later.

Collapse
 
macsikora profile image
Pragmatic Maciej

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.

Collapse
 
gillchristian profile image
Christian Gill

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?

Collapse
 
shimmer profile image
Brian Berns

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!

Collapse
 
gillchristian profile image
Christian Gill

Glad it was useful :)

Collapse
 
efertone profile image
Balazs Nadasdi

I really enjoy the whole series, keep it up, it's so much fun to read and learn a bit more all the time ;)

Collapse
 
gillchristian profile image
Christian Gill • Edited

It mostly me sharing what I'm learning, so I'm really glad it helps somebody else :)

Collapse
 
efertone profile image
Balazs Nadasdi

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 ;)

Thread Thread
 
gillchristian profile image
Christian Gill

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.