Christian Gill

Why Applicative?

Originally posted on dev.to/gillchristian.

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  => (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 🎉


almost 4 years ago

Christian Gill