Applicative functors, or simply Applicatives, are a fundamental concept in Haskell that bridge the gap between Functors and Monads. Applicative functors allow us to apply functions wrapped in a context (like Maybe
, List
, or IO
) to values that are also wrapped in a context. They add a layer of flexibility to functional programming in Haskell, providing a more powerful and expressive way to work with computations in a context.
This article will walk through the essentials of applicative functors, why they’re useful, and how to work with them in Haskell.
- The Basics: What is an Applicative?
- Understanding pure and <*>
- Example: Using Applicatives with Maybe
- Applicatives vs. Functors and Monads
- Applicative Syntax: liftA2 and <$>
- Applicative Instances for Common Types
- Practical Applications of Applicative Functors
- Explain Applicative Functors Like I’m Five Years Old (ELI5)
- Conclusion
The Basics: What is an Applicative?
An applicative is an abstraction that allows you to apply functions within a context to arguments also within a context. This concept is captured by the Applicative
type class, which builds upon the Functor
class in Haskell. Functors allow us to map a function over a value in a context, but Applicatives take it further by enabling functions that are themselves in a context to be applied to values in a context.
The Applicative
type class is defined as follows:
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
The pure
function takes a value and puts it into a minimal default context for the applicative type f
. The operator <*>
(pronounced “apply”) takes a function wrapped in a context and applies it to a value wrapped in the same context, producing a new wrapped result.
Understanding pure
and <*>
pure
: This function takes a regular value and lifts it into an applicative context. For instance,pure 5
for theMaybe
type would give usJust 5
.<*>
: This operator is the core of applicative functors. It takes a function inside a context (likeMaybe (a -> b)
) and applies it to a value inside the same context (likeMaybe a
), resulting in a new value inside that context (Maybe b
).
Example: Using Applicatives with Maybe
Let’s look at an example using Maybe
, a type that represents computations that might fail. We’ll use applicative functors to apply a wrapped function to wrapped values.
-- Adding two values in the context of `Maybe`
addMaybe :: Maybe Int -> Maybe Int -> Maybe Int
addMaybe mx my = pure (+) <*> mx <*> my
In this example:
pure (+)
wraps the addition function(+)
into aMaybe
context:Maybe (Int -> Int -> Int)
.<*>
appliesMaybe (Int -> Int -> Int)
tomx
(aMaybe Int
) and then tomy
(anotherMaybe Int
).
Here’s how addMaybe
works with different inputs:
addMaybe (Just 3) (Just 5) -- Result: Just 8
addMaybe (Just 3) Nothing -- Result: Nothing
addMaybe Nothing (Just 5) -- Result: Nothing
If any argument is Nothing
, the whole expression evaluates to Nothing
, demonstrating how the applicative functor handles failure gracefully by propagating it.
Applicatives vs. Functors and Monads
Applicatives are a middle ground between Functors and Monads. Here’s a comparison of these three abstractions:
- Functor (
fmap
): Allows you to map a regular function over a value in a context. - Applicative (
<*>
): Allows you to apply a function in a context to a value in a context, enabling functions of multiple arguments to work within a context. - Monad (
>>=
): Allows chaining of computations in a context, where each computation can depend on the previous one.
Unlike Monads, applicatives don’t allow for dependencies between operations, meaning each argument is computed independently before applying the function. This property makes Applicatives suitable for situations where computations can be parallelized or are independent of each other.
Applicative Syntax: liftA2
and <$>
To simplify the application of functions to multiple arguments, Haskell provides some helper functions:
liftA2
: Takes a binary function and two applicative values, applying the function to them.
import Control.Applicative (liftA2)
addMaybe' :: Maybe Int -> Maybe Int -> Maybe Int
addMaybe' = liftA2 (+)
-- Equivalent to: addMaybe (Just 3) (Just 5) -> Just 8
<$>
: An infix version of fmap
, used for lifting a function to work with an applicative.
addThree :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int
addThree mx my mz = (+) <$> mx <*> my <*> mz
Here, <$>
applies +
to the first Maybe
value (mx
) and <*>
sequentially applies the resulting function to my
and then mz
.
Applicative Instances for Common Types
Applicative functors are commonly used with standard types like Maybe
, List
, and IO
. Let’s look at how they behave with some of these types:
1. Maybe
Applicative
The Maybe
applicative handles failure by propagating Nothing
:
pure (+) <*> Just 3 <*> Just 5 -- Result: Just 8
pure (+) <*> Just 3 <*> Nothing -- Result: Nothing
2. List
Applicative
The List
applicative applies functions in a context to all combinations of values:
pure (+) <*> [1, 2] <*> [10, 20] -- Result: [11, 21, 12, 22]
Each function application is computed for every combination, similar to a Cartesian product.
3. IO
Applicative
With IO
, applicative functors allow chaining of effects in a controlled way:
getName :: IO String
getName = pure (++) <*> getLine <*> getLine
Here, getName
concatenates two lines of input from the user.
Practical Applications of Applicative Functors
- Validation: Applicative functors can validate multiple fields independently without stopping at the first error, which is common in form or input validation.
import Control.Applicative (liftA2)
validate :: Maybe String -> Maybe Int -> Maybe (String, Int)
validate name age = liftA2 (,) name age
2. Combining Effects: Applicatives are useful when combining effects without dependencies, such as gathering input from multiple sources or combining independent computations.
3. Parallel Computations: Since applicative arguments are independent, they are a natural fit for parallelizable computations.
Explain Applicative Functors Like I’m Five Years Old (ELI5)
Alright! Imagine you have a magic box, and in this magic box, you can put toys or other things you like. Sometimes, you have a toy in one box and a tool in another box that you want to use with that toy.
Now, let’s say you have a box with a teddy bear inside. And you also have another magic box that has a “hugging machine” inside, which is like a little arm that gives the teddy bear a hug.
Here’s the problem: since both the teddy bear and the hugging machine are each in their own magic boxes, they can’t “see” each other. They’re stuck inside their boxes!
With an applicative, you get a way to let these boxes work together! You can take the hugging machine out of its box, and it will know just how to find and hug the teddy bear in its box! So, with applicatives, you get a special helper that knows how to combine things from separate boxes.
This way, your teddy bear gets a hug from the hugging machine, and everyone’s happy!
In Haskell, applicatives help us take a function (like “hug”) that’s inside a box and use it with things in other boxes, so we can make things work together even when they’re in different magic boxes. Cool, right?
Conclusion
Applicative functors provide a powerful and flexible way to handle computations within a context in Haskell. By using pure
and <*>
, applicatives enable functions with multiple arguments to work with values in contexts like Maybe
, List
, and IO
, making them highly versatile for a range of applications. Although they may seem complex at first, applicatives bridge the gap between Functors and Monads, offering a structured approach to dealing with context-dependent computations in a functional programming style.
By mastering applicatives, you unlock an essential tool for creating expressive, reusable, and composable code in Haskell. Whether you’re validating input, combining independent computations, or working with effects, applicative functors provide a robust and elegant solution for handling context in functional programming.
Leave a Reply