What’s the difference between functors and applicatives (applicative functors)?

In Haskell, both functors and applicative functors are abstractions for working with values in a context, such as Maybe, List, or IO. While they are closely related, applicative functors extend the capabilities of functors, enabling more complex operations that require multiple values in a context. Let’s explore the differences between the two concepts and how they’re used.

1. Functors: Applying a Function to a Single Value in a Context

A functor is a type that implements the Functor type class, which provides the fmap function (or the <$> operator). Functors allow you to apply a function to a value inside a context, but the function can only take one argument.

The Functor type class has the following definition:

class Functor f where
    fmap :: (a -> b) -> f a -> f b
  • fmap takes a function (a -> b) and a functor f a, and applies the function to the value inside the functor, producing a new functor f b.

Example with Maybe

The Maybe type is a functor, which allows us to use fmap to apply a function to a Maybe value.

fmap (+1) (Just 5)   -- Result: Just 6
fmap (+1) Nothing    -- Result: Nothing

With fmap, we can apply a function to a value inside the Maybe context, but only to a single Maybe value at a time. If we have multiple Maybe values and a function that requires more than one argument, fmap alone isn’t enough.

2. Applicative Functors: Applying a Function to Multiple Values in a Context

An applicative functor is a type that implements the Applicative type class, which builds on Functor and introduces the pure and <*> functions. Applicative functors allow you to apply a function that’s inside a context to values in other contexts, making it possible to apply multi-argument functions to multiple values within a context.

The Applicative type class is defined as:

class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b
  • pure takes a regular value and lifts it into the applicative context, creating a value of type f a.
  • <*> (pronounced “apply”) takes a function in a context f (a -> b) and a value in the same context f a, and applies the function to the value, producing f b.

Example with Maybe

Suppose we have a function that takes two integers and adds them, and we want to apply it to two Maybe Int values. With applicatives, we can use pure to lift the function into the Maybe context, and then use <*> to apply it to each Maybe Int:

add :: Int -> Int -> Int
add x y = x + y

-- Using pure and <*>
result :: Maybe Int
result = pure add <*> Just 3 <*> Just 5   -- Result: Just 8

Here’s how this works step-by-step:

  1. pure add lifts the add function into the Maybe context, giving us Maybe (Int -> Int -> Int).
  2. <*> applies the partially applied function Just (add 3) to Just 5, resulting in Just 8.

If any of the Maybe values were Nothing, the result would be Nothing, because <*> will propagate failure across all arguments.

Key Differences Between Functors and Applicative Functors

  1. Capabilities:
    • Functors allow you to map a single-argument function over a single value in a context (using fmap).
    • Applicative functors allow you to apply multi-argument functions to multiple values in a context (using <*>), enabling more complex operations.
  2. Functions Inside a Context:
    • With functors, you can only apply functions that take regular arguments to values inside a context.
    • With applicative functors, you can apply functions that are themselves inside a context to other values in a context.
  3. Combining Multiple Contexts:
    • Functors work well for transformations involving a single value in a context.
    • Applicative functors allow combining and processing multiple values in a context (such as Just 3 and Just 5) in a straightforward way.

Summary

To summarize:

  • Functors: Use fmap to apply a function to a single value in a context, such as fmap (+1) (Just 5), which gives Just 6.
  • Applicative Functors: Extend functors by allowing functions with multiple arguments in a context to be applied to multiple values in the same context using pure and <*>, such as pure (+) <*> Just 3 <*> Just 5, which gives Just 8.

In essence, applicative functors are more powerful than functors because they enable working with multiple values in a context at once, making them ideal for operations where multiple values are wrapped in contexts like Maybe, List, or IO. This added flexibility makes applicative functors useful for a wide range of computations in functional programming.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *